init project first version

This commit is contained in:
Robert Jeutter 2021-06-17 20:31:10 +02:00
commit 60bf85d0d3
52 changed files with 17943 additions and 0 deletions

3
.browserslistrc Normal file
View File

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

29
.eslintrc.js Normal file
View File

@ -0,0 +1,29 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended'
],
parserOptions: {
ecmaVersion: 2020
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/no-deprecated-slot-attribute': 'off'
},
overrides: [
{
files: [
'**/__tests__/*.js?(x)',
'**/tests/unit/**/*.spec.js?(x)'
],
env: {
jest: true
}
}
]
}

438
.gitignore vendored Normal file
View File

@ -0,0 +1,438 @@
# Specifies intentionally untracked files to ignore when using Git
# http://git-scm.com/docs/gitignore
*~
*.sw[mnpcod]
.tmp
*.tmp
*.tmp.*
*.sublime-project
*.sublime-workspace
.DS_Store
Thumbs.db
UserInterfaceState.xcuserstate
$RECYCLE.BIN/
*.log
log.txt
npm-debug.log*
/.idea
/.ionic
/.sass-cache
/.sourcemaps
/.versions
/.vscode
/coverage
/dist
/node_modules
/platforms
/plugins
/www
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
## Core latex/pdflatex auxiliary files:
*.aux
*.lof
*.log
*.lot
*.fls
*.out
*.toc
*.fmt
*.fot
*.cb
*.cb2
.*.lb
## Intermediate documents:
*.dvi
*.xdv
*-converted-to.*
# these rules might exclude image files for figures etc.
# *.ps
# *.eps
# *.pdf
## Generated if empty string is given at "Please type another file name for output:"
.pdf
## Bibliography auxiliary files (bibtex/biblatex/biber):
*.bbl
*.bcf
*.blg
*-blx.aux
*-blx.bib
*.run.xml
## Build tool auxiliary files:
*.fdb_latexmk
*.synctex
*.synctex(busy)
*.synctex.gz
*.synctex.gz(busy)
*.pdfsync
## Build tool directories for auxiliary files
# latexrun
latex.out/
## Auxiliary and intermediate files from other packages:
# algorithms
*.alg
*.loa
# achemso
acs-*.bib
# amsthm
*.thm
# beamer
*.nav
*.pre
*.snm
*.vrb
# changes
*.soc
# comment
*.cut
# cprotect
*.cpt
# elsarticle (documentclass of Elsevier journals)
*.spl
# endnotes
*.ent
# fixme
*.lox
# feynmf/feynmp
*.mf
*.mp
*.t[1-9]
*.t[1-9][0-9]
*.tfm
#(r)(e)ledmac/(r)(e)ledpar
*.end
*.?end
*.[1-9]
*.[1-9][0-9]
*.[1-9][0-9][0-9]
*.[1-9]R
*.[1-9][0-9]R
*.[1-9][0-9][0-9]R
*.eledsec[1-9]
*.eledsec[1-9]R
*.eledsec[1-9][0-9]
*.eledsec[1-9][0-9]R
*.eledsec[1-9][0-9][0-9]
*.eledsec[1-9][0-9][0-9]R
# glossaries
*.acn
*.acr
*.glg
*.glo
*.gls
*.glsdefs
*.lzo
*.lzs
# uncomment this for glossaries-extra (will ignore makeindex's style files!)
# *.ist
# gnuplottex
*-gnuplottex-*
# gregoriotex
*.gaux
*.glog
*.gtex
# htlatex
*.4ct
*.4tc
*.idv
*.lg
*.trc
*.xref
# hyperref
*.brf
# knitr
*-concordance.tex
# TODO Uncomment the next line if you use knitr and want to ignore its generated tikz files
# *.tikz
*-tikzDictionary
# listings
*.lol
# luatexja-ruby
*.ltjruby
# makeidx
*.idx
*.ilg
*.ind
# minitoc
*.maf
*.mlf
*.mlt
*.mtc[0-9]*
*.slf[0-9]*
*.slt[0-9]*
*.stc[0-9]*
# minted
_minted*
*.pyg
# morewrites
*.mw
# newpax
*.newpax
# nomencl
*.nlg
*.nlo
*.nls
# pax
*.pax
# pdfpcnotes
*.pdfpc
# sagetex
*.sagetex.sage
*.sagetex.py
*.sagetex.scmd
# scrwfile
*.wrt
# sympy
*.sout
*.sympy
sympy-plots-for-*.tex/
# pdfcomment
*.upa
*.upb
# pythontex
*.pytxcode
pythontex-files-*/
# tcolorbox
*.listing
# thmtools
*.loe
# TikZ & PGF
*.dpth
*.md5
*.auxlock
# todonotes
*.tdo
# vhistory
*.hst
*.ver
# easy-todo
*.lod
# xcolor
*.xcp
# xmpincl
*.xmpi
# xindy
*.xdy
# xypic precompiled matrices and outlines
*.xyc
*.xyd
# endfloat
*.ttt
*.fff
# Latexian
TSWLatexianTemp*
## Editors:
# WinEdt
*.bak
*.sav
# Texpad
.texpadtmp
# LyX
*.lyx~
# Kile
*.backup
# gummi
.*.swp
# KBibTeX
*~[0-9]*
# TeXnicCenter
*.tps
# auto folder when using emacs and auctex
./auto/*
*.el
# expex forward references with \gathertags
*-tags.tex
# standalone packages
*.sta
# Makeindex log files
*.lpz
# xwatermark package
*.xwm
# REVTeX puts footnotes in the bibliography by default, unless the nofootinbib
# option is specified. Footnotes are the stored in a file with suffix Notes.bib.
# Uncomment the next line to have this generated file ignored.
#*Notes.bib

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2011-present, Modus Create, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

57
Readme-EN.md Normal file
View File

@ -0,0 +1,57 @@
# CocktailShaker App
Project in the context of the event [Content Exploitation Models and their Implementation in Mobile Systems](https://www.tu-ilmenau.de/modultafeln/Informatik/Bachelor/2013/fach/13137/) under Dr. Jürgen Nützel
## Warning
The project is not commercial. It is just a report of a private event. All the information, resources or brand names found in the text or appearing in the photos are provided for information and demonstration purposes only. Any types of direct or indirect marketing are not assumed.
The authors of the project warn about the damage alcohol does and don't take any responsibility for a result of the use of any content posted.
All the cocktail recipes are for information only.
No food bloggers, bartenders or programmers were harmed in the making of this project.
## Contribution
- the (CocktailDB)[https://www.thecocktaildb.com/] API; you can add your own API Key
- Cocktails: [International Bartenders Association (Teijo)](https://github.com/teijo/iba-cocktails)
- [Cocktailpictures](https://github.com/alfg/opendrinks)
- [Glasses](https://github.com/mikeyhogarth/cocktails)
- Library for Apps [Ionic](https://github.com/ionic-team/ionic)
- JS Framework [Vue](https://github.com/vuejs/vue)
- cross-platform compability [Capacitor](https://github.com/ionic-team/capacitor)
## Develop locally
1. download the installer](https://nodejs.org/) for Node LTS
2. install the ionic CLI globally: `npm install -g ionic`
3. clone this repository: `git clone https://github.com/wieerwill/cocktailshaker-app.git`
4. move to folder `cd cocktailshaker-app`
5. run `npm install` from the project root
6. run `ionic serve` in a terminal from the project root
7. follow the link in the console to view the app in the browser or smartphone
### Deploy
To deploy everything to a production-ready app, run this command:
```sh
ionic build
```
This will build and update all files in the `dist/` folder
### Build general
1. update the Capacitor config after each standard build process: `ionic cap copy`
2. sync Capacitor builds after each new Plugin or huge code change: `ionic cap sync`
- Android build
1. you need [Android SDK](https://developer.android.com/studio/).
- the easiest way on a Mac is `homebrew`: `brew install android-sdk`
- on Linux you can use the package manager: `sudo apt-get install android-sdk` or via
- [Flatpak](https://flathub.org/apps/details/com.google.AndroidStudio) or
- [Snap](https://uappexplorer.com/snap/ubuntu/android-studio)
2. open the app in your AndroidStudio `ionic cap open android`
3. to publish the app you need to [sign](https://developer.android.com/studio/publish/app-signing) it. For local testing, a sample file is available at
```sh
cp android/signing/keystore.properties.example
```
4. you may need to adjust the value of `storeFile` for your platform
```sh
storeFile=~/.android/debug.keystore
```
- iOS build open in XCode `ionic cap open ios`
# License
The project runs under the [MIT](./LICENSE) licence.

64
Readme.md Normal file
View File

@ -0,0 +1,64 @@
# CocktailShaker App
[English Readme](Readme-EN.md)
Ein Projekt für die Veranstaltung [Content Verwertungsmodelle und ihre Umsetzung in mobilen Systemen](https://www.tu-ilmenau.de/modultafeln/Informatik/Bachelor/2013/fach/13137/) unter Dr. Jürgen Nützel.
Für den Rahmen der App war vorgegeben:
- eine Native oder Hybride App für mobile Endgeräte
- mindestens ein Sensor des Endgeräts muss verwendet werden (Kamera, Geolocation,...)
- die App muss sich mit einem Server verbinden (keine offline App)
## Warnung
Das Projekt ist nicht kommerziell. Es ist lediglich ein Projekt während einer universitären Veranstaltung.
Alle Informationen, Ressourcen oder Markennamen, die im Text vorkommen oder auf den Fotos zu sehen sind, werden nur zu Informations- und Demonstrationszwecken bereitgestellt. Jegliche Art von direktem oder indirektem Marketing wird nicht angenommen.
Die Autoren des Projekts warnen vor den Schäden, die Alkohol anrichtet, und übernehmen keine Verantwortung für ein Ergebnis der Verwendung der eingestellten Inhalte.
Alle Cocktailrezepte dienen nur zur Information.
Bei der Erstellung dieses Projekts wurden keine Food-Blogger, Barkeeper oder Programmierer geschädigt.
## Quellen und Bibliotheken
- die (CocktailDB)[https://www.thecocktaildb.com/] API; der eigene API Schlüssel kann in der App gespeichert werden
- Cocktails der [International Bartenders Association (Teijo)](https://github.com/teijo/iba-cocktails)
- [Gläser](https://github.com/mikeyhogarth/cocktails)
- Bibliothek für Apps [Ionic](https://github.com/ionic-team/ionic)
- JS Framework [Vue](https://github.com/vuejs/vue)
- Kompabilität für mobile Systeme [Capacitor](https://github.com/ionic-team/capacitor)
## Lokal weiterentwickeln
1. Installiere [NodeJS](https://nodejs.org/) in der aktuellen LTS Version (>=14.15)
2. Installiere die Ionic CLI ionic global: `npm install -g ionic`
3. Klone das Repository `git clone https://github.com/wieerwill/cocktailshaker-app.git`
4. In den neuen Ordner wechseln `cd cocktailshaker-app`
5. Pakete mit `npm install` installieren
6. den Entwicklungsserver mit `ionic serve` starten
7. Folge dem Link in der Konsole um die App im Browser oder Smartphone zu betrachten
## Deploy
Um alles für eine Produktionsfertige App bereitzustellen führe diesen Befehl aus:
```sh
npm run build
```
Das wird alle Dateien im `dist/` Ordner erstellen und updaten
### Build general
1. bei jedem Build Prozess müssen die Capacitor Ordner aktualisiert werden: `ionic cap copy`
2. nach Updates oder großen Änderungen des Codes (neues Plugin) muss Capacitor synchronisiert werden: `ionic cap sync`
- Android build
1. Du benötigst die [Android SDK](https://developer.android.com/studio/).
- der einfachste Weg auf einem Mac ist `homebrew`: `brew install android-sdk`
- auf Linux kann man den Paketmanager nutzen: `sudo apt-get install android-sdk` or via
- [Flatpak](https://flathub.org/apps/details/com.google.AndroidStudio) or
- [Snap](https://uappexplorer.com/snap/ubuntu/android-studio)
2. die App im Android Studio öffnen `ionic cap open android`
3. Um die App zu veröffentlichen benötigst musst du diese [signieren](https://developer.android.com/studio/publish/app-signing). Für lokale Tests ist eine Beispieldatei unter
```sh
cp android/signing/keystore.properties.example
```
4. Möglicherweise musst du den Wert von `storeFile` für deine Platform anpassen
```sh
storeFile=~/.android/debug.keystore
```
- iOS build in XCode öffnen `ionic cap open ios`
## Lizenz
Das Projekt läuft unter der [MIT](./LICENSE) Lizenz.

5
babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

13
capacitor.config.json Normal file
View File

@ -0,0 +1,13 @@
{
"appId": "io.ionic.starter",
"appName": "cocktailShakerApp",
"bundledWebRuntime": false,
"npmClient": "yarn",
"webDir": "dist",
"plugins": {
"SplashScreen": {
"launchShowDuration": 0
}
},
"cordova": {}
}

3
cypress.json Normal file
View File

@ -0,0 +1,3 @@
{
"pluginsFile": "tests/e2e/plugins/index.js"
}

7
ionic.config.json Normal file
View File

@ -0,0 +1,7 @@
{
"name": "cocktailShakerApp",
"integrations": {
"capacitor": {}
},
"type": "vue"
}

7
jest.config.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
transform: {
'^.+\\.vue$': 'vue-jest'
},
transformIgnorePatterns: ['/node_modules/(?!@ionic/vue|@ionic/vue-router)']
}

52
package.json Normal file
View File

@ -0,0 +1,52 @@
{
"name": "cocktail-shaker-app",
"author": "robert jeutter",
"version": "0.0.1",
"private": true,
"description": "shake your way trough the cocktailbar with this multi-platform app",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"test:e2e": "vue-cli-service test:e2e",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@capacitor/android": "2.4.7",
"@capacitor/core": "2.4.7",
"@capacitor/ios": "2.4.7",
"@ionic-native/core": "^5.33.1",
"@ionic-native/social-sharing": "^5.33.1",
"@ionic/cli": "^6.15.0",
"@ionic/pwa-elements": "^3.0.2",
"@ionic/storage": "^3.0.4",
"@ionic/vue": "^5.4.0",
"@ionic/vue-router": "^5.4.0",
"@vueuse/core": "^5.0.2",
"axios": "^0.21.1",
"cordova-plugin-x-socialsharing": "^6.0.3",
"core-js": "^3.6.5",
"es6-promise-plugin": "^4.2.2",
"register-service-worker": "^1.7.1",
"vue": "^3.0.0-0",
"vue-router": "^4.0.0-0"
},
"devDependencies": {
"@capacitor/cli": "2.4.7",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-e2e-cypress": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-pwa": "^4.5.13",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-unit-jest": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0-0",
"@vue/test-utils": "^2.0.0-0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0-0",
"vue-jest": "^5.0.0-0"
},
"resolutions": {
"cypress": "^4.4.0"
}
}

BIN
public/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1" baseProfile="full"
width="25" height="25"
viewBox="25 25">
<title>Champagne Flute</title>
<path d="M 9 2 C 8.843549 2 10.143933 12.844562 11.75 13.908203 L 11.75 22.113281 A 4 0.33162412 0 0 0 8 22.443359 A 4 0.33162412 0 0 0 12 22.775391 A 4 0.33162412 0 0 0 16 22.443359 A 4 0.33162412 0 0 0 12.25 22.113281 L 12.25 13.908203 C 13.843978 12.84149 15.027726 2 15 2 L 9 2 z " />
</svg>

After

Width:  |  Height:  |  Size: 502 B

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1" baseProfile="full"
width="25" height="25"
viewBox="25 25">
<title>Champagne Tulip</title>
<path d="M 10 2 C 8.4883687 7.641485 10.10868 13.398989 11.75 13.945312 L 11.75 22.113281 A 4 0.33162412 0 0 0 8 22.443359 A 4 0.33162412 0 0 0 12 22.775391 A 4 0.33162412 0 0 0 16 22.443359 A 4 0.33162412 0 0 0 12.25 22.113281 L 12.25 13.945312 C 13.89132 13.398989 15.511632 7.641485 14 2 L 10 2 z " />
</svg>

After

Width:  |  Height:  |  Size: 522 B

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1" baseProfile="full"
width="25" height="25"
viewBox="25 25">
<title>Collins</title>
<path d="M 7,2 H 17 V 24 H 7 Z" />
</svg>

After

Width:  |  Height:  |  Size: 240 B

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1" baseProfile="full"
width="25" height="25"
viewBox="25 25">
<title>Highball</title>
<path d="M 2,0 H 22 L 19,24 H 5 Z" />
</svg>

After

Width:  |  Height:  |  Size: 248 B

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1" baseProfile="full"
width="25" height="25"
viewBox="25 25">
<title>Hot Drink</title>
<path d="M 20,5.5 H 4 v 10 c 0,2.21 1.79,4 4,4 h 6 c 2.21,0 4,-1.79 4,-4 v -3 h 2 c 1.11,0 2,-0.9 2,-2 v -3 c 0,-1.11 -0.89,-2 -2,-2 z m 0,5 h -2 v -3 h 2 z m -16,11 h 16 v 2 H 4 Z" />
</svg>

After

Width:  |  Height:  |  Size: 396 B

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1" baseProfile="full"
width="25" height="25"
viewBox="25 25">
<title>Hurricane</title>
<path d="M 17.959,0.5 H 6 c 0,0 1.953125,4.3172276 1.953125,6.8320312 0,2.2907126 -2.6559146,4.5223298 -1.7792969,6.6386718 C 6.8846738,15.686836 11,19.814453 11,19.814453 v 2.917969 L 6,23.5 H 17.958984 L 13,22.732422 v -2.917969 c 0,0 4.236581,-4.090798 4.958984,-5.84375 C 18.835479,11.843839 16.100682,9.6322208 16.070312,7.3320312 16.036822,4.7954668 17.959,0.5 17.959,0.5 Z" />
</svg>

After

Width:  |  Height:  |  Size: 595 B

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1" baseProfile="full"
width="25" height="25"
viewBox="25 25">
<title>Margarita</title>
<path d="M 0 0 C 0 4.0686355 2.0299978 6.5292006 5.1269531 7.8261719 C 5.4472547 11.625511 6.6408432 13.70419 11 13.966797 L 11 22 L 12 22 L 13 22 L 13 13.966797 C 17.359157 13.70419 18.552745 11.625511 18.873047 7.8261719 C 21.970002 6.5292006 24 4.0686355 24 0 C 19.113636 0.15886848 10.482798 0.03662556 0 0 z M 12 22 C 7.4967668 22 3.9999997 23.249461 4 24 C 9.0181747 24.03423 14.314143 24.228861 20 24 C 20 23.249461 16.503233 22 12 22 z " />
</svg>

After

Width:  |  Height:  |  Size: 660 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1" baseProfile="full"
width="25" height="25"
viewBox="25 25">
<title>Martini</title>
<path d="m 21,7.5 v -2 H 3 v 2 l 8,9 v 5 H 6 v 2 h 12 v -2 h -5 v -5 z m -13.57,2 -1.77,-2 h 12.69 l -1.78,2 z" />
</svg>

After

Width:  |  Height:  |  Size: 321 B

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1" baseProfile="full"
width="25" height="25"
viewBox="25 25">
<title>Old Fashioned</title>
<path d="M 4,6 H 20 V 24 H 4 Z" />
</svg>

After

Width:  |  Height:  |  Size: 250 B

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1" baseProfile="full"
width="25" height="25"
viewBox="25 25">
<title>Shot</title>
<path d="M 4,10.344 H 20 L 18,24 H 6 Z" />
</svg>

After

Width:  |  Height:  |  Size: 245 B

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1" baseProfile="full"
width="25" height="25"
viewBox="25 25">
<title>White Wine</title>
<path d="m 2,0 c 0.8985058,5.2114236 5.5451555,12.729721 9,13 v 9 h 1 1 V 13 C 17.626082,12.225887 21.941651,3.0666783 22,0 17.113636,0.15886848 12.482798,0.03662556 2,0 Z m 10,22 c -4.5032332,0 -8.0000003,1.249461 -8,2 5.0181747,0.03423 10.314143,0.228861 16,0 0,-0.750539 -3.496767,-2 -8,-2 z" />
</svg>

After

Width:  |  Height:  |  Size: 507 B

BIN
public/img/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
public/img/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

27
public/index.html Normal file
View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Cocktail Shaker App</title>
<base href="/" />
<meta name="color-scheme" content="light dark" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no , viewport-fit=cover" />
<meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" />
<link rel="shortcut icon" type="image/png" href="<%= BASE_URL %>img/favicon.ico" />
<!-- add to homescreen for ios -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="Cocktail Shaker App" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
</head>
<body>
<div id="app"></div>
</body>
</html>

27
public/manifest.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "cocktail-shaker",
"short_name": "CS",
"theme_color": "#4DBA87",
"icons": [{
"src": "./img/favicon.ico",
"sizes": "32x32",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "./img/icon.png",
"sizes": "32x32",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "./img/splash.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable"
}
],
"start_url": ".",
"display": "standalone",
"background_color": "#000000"
}

2
public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow:

18
src/App.vue Normal file
View File

@ -0,0 +1,18 @@
<template>
<ion-app>
<ion-router-outlet />
</ion-app>
</template>
<script>
import { IonApp, IonRouterOutlet } from '@ionic/vue';
import { defineComponent } from 'vue';
export default defineComponent({
name: 'App',
components: {
IonApp,
IonRouterOutlet
}
});
</script>

View File

@ -0,0 +1,31 @@
import { Plugins } from '@capacitor/core';
const { Geolocation } = Plugins;
let position;
export function useGeolocation() {
const updatePosition = async () => {
position = await Geolocation.getCurrentPosition()
}
const getCurrentPosition = () => {
updatePosition();
return position;
}
const getLatitude = () => {
updatePosition();
return position.coords.latitude;
}
const getLongitude = () => {
updatePosition();
return position.coords.longitude;
}
return {
getCurrentPosition,
getLatitude,
getLongitude
}
}

View File

@ -0,0 +1,81 @@
import {
Plugins
} from '@capacitor/core';
const {
Motion
} = Plugins;
export function useMotion() {
let acceleration = {
x: 0,
y: 0,
z: 0,
}
const initMotionSensor = async () => {
try {
await Motion.requestPermissions
} catch (e) {
// Handle error
console.log("coudn't get motion sensor permission")
return false
}
console.log("success");
return true
}
const getMotion = async () => {
//Motion.requestPermissions
//initMotionSensor()
Motion.addListener('accel', (event) => {
console.log(
"x: ", event.acceleration.x,
"\ny: ", event.acceleration.y,
"\nz: ", event.acceleration.z
)
});
return 12
}
const detectShake = () => {
this.subscription = Motion.watchAcceleration({
frequency: 200
}).subscribe(acc => {
console.log(acc);
if (!this.lastX) {
this.lastX = acc.x;
this.lastY = acc.y;
this.lastZ = acc.z;
return;
}
let deltaX, deltaY, deltaZ;
deltaX = Math.abs(acc.x - this.lastX);
deltaY = Math.abs(acc.y - this.lastY);
deltaZ = Math.abs(acc.z - this.lastZ);
if (deltaX + deltaY + deltaZ > 3) {
this.moveCounter++;
} else {
this.moveCounter = Math.max(0, --this.moveCounter);
}
if (this.moveCounter > 2) {
console.log('SHAKE');
this.moveCounter = 0;
}
this.lastX = acc.x;
this.lastY = acc.y;
this.lastZ = acc.z;
});
}
return {
coord,
initMotionSensor,
getMotion,
detectShake
}
}

View File

@ -0,0 +1,287 @@
import {
ref,
onMounted,
} from 'vue';
import {
Plugins,
CameraResultType,
CameraSource,
FilesystemDirectory,
Capacitor
} from "@capacitor/core";
import {
isPlatform
} from '@ionic/vue';
import axios from 'axios';
import cocktailJSON from "../data/cocktails.json";
let cocktails = ref([]);
const cocktailStorage = "cocktails";
let apiKey = "1";
const apiKeyStorage = "cocktailDBapiKey";
export function useStorage() {
const {
Camera,
Filesystem,
Storage
} = Plugins;
const cacheCocktails = () => {
Storage.set({
key: cocktailStorage,
value: JSON.stringify(cocktails.value)
});
}
const loadSaved = async () => {
const cocktailList = await Storage.get({
key: cocktailStorage
});
const cocktailsInStorage = cocktailList.value ? JSON.parse(cocktailList.value) : [];
// If running on the web...
if (!isPlatform('hybrid')) {
for (const cocktail of cocktailsInStorage) {
if (cocktail.image) {
const file = await Filesystem.readFile({
path: cocktail.image.filepath,
directory: FilesystemDirectory.Data
});
// Web platform only: Load the photo as base64 data
cocktail.image.webviewPath = `data:image/jpeg;base64,${file.data}`;
}
}
}
cocktails.value = cocktailsInStorage;
}
onMounted(loadSaved);
const restoreCocktails = () => {
//console.log("restoring cocktails")
cocktailJSON.sort(function(a, b) {
return a.name.localeCompare(b.name)
})
cocktails.value = cocktailJSON;
}
const getRandomCocktail = () => {
fetchRandomCocktail();
return cocktails.value[Math.random() * cocktails.value.length]
}
const getCocktail = (id) => {
//console.log(id)
let cocktail = cocktails.value[id]
return cocktail
}
const favouriseCocktail = (cocktail) => {
let cocktailNr = cocktails.value.indexOf(cocktail)
cocktails.value[cocktailNr].favourite = !cocktails.value[cocktailNr].favourite;
cacheCocktails()
}
const addCocktail = (cocktail) => {
let newCocktail = {
"name": cocktail.name,
"ingredients": cocktail.ingredients,
"directions": cocktail.directions,
"glass": cocktail.glass,
"author": cocktail.author ? cocktail.author : "You"
}
let tempCocktailList = [newCocktail, ...cocktails.value]
tempCocktailList.sort(function(a, b) {
return a.name.localeCompare(b.name)
})
cocktails.value = tempCocktailList
cacheCocktails()
}
const editCocktail = (cocktail) => {
let cocktailNr = cocktails.value.indexOf(cocktail)
cocktails.value[cocktailNr] = cocktail;
cacheCocktails()
}
const removeCocktail = async (cocktail) => {
if (cocktail.image) {
// delete photo file from filesystem
const filename = cocktail.image.filepath.substr(cocktail.image.filepath.lastIndexOf('/') + 1);
await Filesystem.deleteFile({
path: filename,
directory: FilesystemDirectory.Data
});
}
cocktails.value.splice(cocktails.value.indexOf(cocktail), 1)
cacheCocktails()
}
const convertBlobToBase64 = (blob) => new Promise((resolve, reject) => {
const reader = new FileReader;
reader.onerror = reject;
reader.onload = () => {
resolve(reader.result);
};
reader.readAsDataURL(blob);
});
const savePicture = async (photo, fileName) => {
let base64Data;
if (isPlatform('hybrid')) {
// "hybrid" will detect mobile - iOS or Android
const file = await Filesystem.readFile({
path: photo.path
});
base64Data = file.data;
} else {
// Fetch the photo, read as a blob, then convert to base64 format
const response = await fetch(photo.webPath);
const blob = await response.blob();
base64Data = await convertBlobToBase64(blob);
}
const savedFile = await Filesystem.writeFile({
path: fileName,
data: base64Data,
directory: FilesystemDirectory.Data
});
if (isPlatform('hybrid')) {
// rewritr the 'file://' path to HTTP
return {
filepath: savedFile.uri,
webviewPath: Capacitor.convertFileSrc(savedFile.uri),
};
} else {
// Use webPath to display image
return {
filepath: fileName,
webviewPath: photo.webPath
};
}
};
const takePhoto = async (cocktail) => {
const cameraPhoto = await Camera.getPhoto({
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
quality: 100
});
const fileName = "cocktailShaker-" + new Date().getTime() + '.jpeg';
const savedFileImage = await savePicture(cameraPhoto, fileName);
//console.log(savedFileImage);
let cocktailNr = cocktails.value.indexOf(cocktail)
cocktails.value[cocktailNr].image = savedFileImage;
cacheCocktails()
};
const cacheApiKey = () => {
Storage.set({
key: apiKeyStorage,
value: apiKey
})
}
const loadApiKey = async () => {
let myApiKey = await Storage.get({
key: apiKeyStorage
});
apiKey = apiKey || myApiKey;
}
onMounted(loadApiKey);
const restoreApiKey = () => {
apiKey = "1";
cacheApiKey();
//console.log(apiKey)
}
const updateApiKey = async (key) => {
//console.log("received key", key.toString())
apiKey = key.toString();
try {
cacheApiKey();
return apiKey
} catch {
return false
}
}
const testApiKey = async () => {
return axios({
method: 'post',
url: `https://www.thecocktaildb.com/api/json/v1/${apiKey}/random.php`,
responseType: 'json'
})
.then(function(response) {
return response.data;
})
.catch(function() {
return "error"
});
}
const fetchRandomCocktail = async () => {
return axios({
method: 'post',
url: `https://www.thecocktaildb.com/api/json/v1/${apiKey}/random.php`,
responseType: 'json'
})
.then(response => {
let fetchedCocktail = {
"name": response.data.drinks[0].strDrink,
"ingredients": [],
"directions": response.data.drinks[0].strInstructions,
"glass": response.data.drinks[0].strGlass.replace("glass", "").replace("Glass", "").trim(),
"author": response.data.drinks[0].strIBA ? "International Bartenders Association" : "CocktailDB"
}
for (var i = 1; i <= 15; i++) {
if (response.data.drinks[0]["strIngredient" + i] != null) {
let amount = null,
unit = null;
if (response.data.drinks[0]["strMeasure" + i] != null) {
amount = response.data.drinks[0]["strMeasure" + i].substr(0, response.data.drinks[0]["strMeasure" + i].indexOf(" "))
unit = response.data.drinks[0]["strMeasure" + i].substr(response.data.drinks[0]["strMeasure" + i].indexOf(" ") + 1);
}
fetchedCocktail.ingredients.push({
"unit": unit,
"amount": amount,
"ingredient": response.data.drinks[0]["strIngredient" + i]
})
}
}
try {
addCocktail(fetchedCocktail);
return true;
} catch {
return "mapping failed"
}
})
.catch(function() {
return "error"
});
}
return {
cocktails,
restoreCocktails,
getRandomCocktail,
getCocktail,
favouriseCocktail,
addCocktail,
editCocktail,
removeCocktail,
takePhoto,
apiKey,
restoreApiKey,
updateApiKey,
testApiKey,
fetchRandomCocktail
}
}

2104
src/data/cocktails.json Normal file

File diff suppressed because it is too large Load Diff

13
src/data/glasses.json Normal file
View File

@ -0,0 +1,13 @@
[
"Martini",
"Old Fashioned",
"Collins",
"Highball",
"Champagne Flute",
"Margarita",
"Champagne Tulip",
"Hurricane",
"Shot",
"Hot-drink Mug",
"White Wine"
]

365
src/data/ingredients.json Normal file
View File

@ -0,0 +1,365 @@
[
{
"name": "Absinthe",
"abv": 40,
"taste": null
},
{
"name": "Agave nectar",
"abv": 0,
"taste": "sweet"
},
{
"name": "Angostura bitters",
"abv": 44,
"taste": "bitter"
},
{
"name": "Aperol",
"abv": 11,
"taste": "bitter"
},
{
"name": "Apricot brandy",
"abv": 40,
"taste": null
},
{
"name": "Blackberry liqueur",
"abv": 40,
"taste": null
},
{
"name": "Blue Curaçao",
"abv": 40,
"taste": "sweet"
},
{
"name": "Cachaca",
"abv": 40,
"taste": null
},
{
"name": "Calvados",
"abv": 40,
"taste": null
},
{
"name": "Campari",
"abv": 25,
"taste": null,
"vegan": false
},
{
"name": "Champagne",
"abv": 12,
"taste": null
},
{
"name": "Cherry liqueur",
"abv": 30,
"taste": null
},
{
"name": "Citron Vodka",
"abv": 40,
"taste": null
},
{
"name": "Coconut milk",
"abv": 0,
"taste": "sweet"
},
{
"name": "Coffee",
"abv": 0,
"taste": "bitter"
},
{
"name": "Coffee liqueur",
"abv": 20,
"taste": "bitter"
},
{
"name": "Cognac",
"abv": 40,
"taste": null
},
{
"name": "Cola",
"abv": 0,
"taste": "bitter"
},
{
"name": "Cranberry juice",
"abv": 0,
"taste": "sour"
},
{
"name": "Cream",
"abv": 0,
"taste": "sweet",
"vegan": false
},
{
"name": "Créme de Cacao",
"abv": 20,
"taste": null
},
{
"name": "Créme de Cassis",
"abv": 15,
"taste": null
},
{
"name": "Créme de Menthe",
"abv": 25,
"taste": null
},
{
"name": "Crème de violette",
"abv": 20,
"taste": null
},
{
"name": "Cream liqueur",
"abv": 20,
"taste": null,
"vegan": false
},
{
"name": "Dark rum",
"abv": 40,
"taste": null
},
{
"name": "DiSaronno",
"abv": 28,
"taste": null
},
{
"name": "DOM Bénédictine",
"abv": 40,
"taste": null,
"vegan": false
},
{
"name": "Drambuie",
"abv": 40,
"taste": null,
"vegan": false
},
{
"name": "Dry White Wine",
"abv": 12,
"taste": null
},
{
"name": "Egg white",
"abv": 0,
"taste": null,
"vegan": false
},
{
"name": "Egg yolk",
"abv": 0,
"taste": null,
"vegan": false
},
{
"name": "Galliano",
"abv": 30,
"taste": "sweet"
},
{
"name": "Gin",
"abv": 40,
"taste": null
},
{
"name": "Ginger Ale",
"abv": 0,
"taste": null
},
{
"name": "Ginger beer",
"abv": 5,
"taste": "sweet"
},
{
"name": "Grapefruit juice",
"abv": 0,
"taste": "sour"
},
{
"name": "Honey",
"abv": 0,
"taste": "sweet",
"vegan": false
},
{
"name": "Kirsch",
"abv": 40,
"taste": null
},
{
"name": "Lemon juice",
"abv": 0,
"taste": "sour"
},
{
"name": "Lillet Blonde",
"abv": 15,
"taste": null
},
{
"name": "Lime",
"abv": 0,
"taste": "sour"
},
{
"name": "Lime juice",
"abv": 0,
"taste": "sour"
},
{
"name": "Mint",
"abv": 0,
"taste": null
},
{
"name": "Olive juice",
"abv": 0,
"taste": "sour"
},
{
"name": "Orange bitters",
"abv": 40,
"taste": null
},
{
"name": "Orange flower water",
"abv": 0,
"taste": null
},
{
"name": "Orange juice",
"abv": 0,
"taste": "sweet"
},
{
"name": "Peach bitters",
"abv": 0,
"taste": "fruity"
},
{
"name": "Peach puree",
"abv": 0,
"taste": "sweet"
},
{
"name": "Peach schnapps",
"abv": 40,
"taste": "sweet"
},
{
"name": "Peychauds bitters",
"abv": 35,
"taste": "woody"
},
{
"name": "Pineapple juice",
"abv": 0,
"taste": "sweet"
},
{
"name": "Pisco",
"abv": 40,
"taste": null
},
{
"name": "Prosecco",
"abv": 12,
"taste": null
},
{
"name": "Raspberry liqueur",
"abv": 20,
"taste": "sweet"
},
{
"name": "Raspberry syrup",
"abv": 0,
"taste": "sweet"
},
{
"name": "Red Port",
"abv": 20,
"taste": null
},
{
"name": "Soda water",
"abv": 0,
"taste": null
},
{
"name": "Sparkling Wine",
"abv": 12,
"taste": null
},
{
"name": "Sugar",
"abv": 0,
"taste": "sweet"
},
{
"name": "Syrup",
"abv": 0,
"taste": "sweet"
},
{
"name": "Tequila",
"abv": 40,
"taste": null
},
{
"name": "Tomato juice",
"abv": 0,
"taste": "salty"
},
{
"name": "Triple Sec",
"abv": 40,
"taste": "sweet"
},
{
"name": "Vanilla extract",
"abv": 0,
"taste": "sweet"
},
{
"name": "Vermouth",
"abv": 17,
"taste": null
},
{
"name": "Vodka",
"abv": 40,
"taste": null
},
{
"name": "Whiskey",
"abv": 40,
"taste": null
},
{
"name": "White rum",
"abv": 40,
"taste": null
},
{
"name": "Worcestershire Sauce",
"abv": 0,
"taste": null
}
]

10
src/data/units.json Normal file
View File

@ -0,0 +1,10 @@
[
"Piece",
"ml",
"cl",
"Dash",
"Splash",
"Teaspoon",
"Barspoon",
"Slice"
]

43
src/main.js Normal file
View File

@ -0,0 +1,43 @@
import {
createApp
} from 'vue'
import App from './App.vue'
import router from './router';
import {
IonicVue
} from '@ionic/vue';
import {
defineCustomElements
} from '@ionic/pwa-elements/loader';
/* Core CSS required for Ionic components to work properly */
import '@ionic/vue/css/core.css';
/* Basic CSS for apps built with Ionic */
import '@ionic/vue/css/normalize.css';
import '@ionic/vue/css/structure.css';
import '@ionic/vue/css/typography.css';
/* Optional CSS utils that can be commented out */
import '@ionic/vue/css/padding.css';
import '@ionic/vue/css/float-elements.css';
import '@ionic/vue/css/text-alignment.css';
import '@ionic/vue/css/text-transformation.css';
import '@ionic/vue/css/flex-utils.css';
import '@ionic/vue/css/display.css';
/* Theme variables */
import './theme/variables.css';
import './registerServiceWorker'
const app = createApp(App)
.use(IonicVue)
.use(router);
router.isReady().then(() => {
app.mount('#app');
});
defineCustomElements(window);

View File

@ -0,0 +1,39 @@
import {
register
} from 'register-service-worker'
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready() {
// eslint-disable-next-line
console.log(
'App is being served from cache by a service worker.\n' +
'For more details, visit https://goo.gl/AFskqB'
)
},
registered() {
// eslint-disable-next-line
console.log('Service worker has been registered.')
},
cached() {
// eslint-disable-next-line
console.log('Content has been cached for offline use.')
},
updatefound() {
// eslint-disable-next-line
console.log('New content is downloading.')
},
updated() {
// eslint-disable-next-line
console.log('New content is available; please refresh.')
},
offline() {
// eslint-disable-next-line
console.log('No internet connection found. App is running in offline mode.')
},
error(error) {
// eslint-disable-next-line
console.error('Error during service worker registration:', error)
}
})
}

60
src/router.js Normal file
View File

@ -0,0 +1,60 @@
import {
createRouter,
createWebHistory
} from '@ionic/vue-router';
import Tabs from './views/Tabs.vue'
const routes = [{
path: '/',
redirect: '/tabs/Shake'
},
{
path: '/tabs/',
component: Tabs,
children: [{
path: '',
redirect: '/tabs/Shake'
},
{
path: 'Shake',
component: () => import('@/views/Shake.vue')
},
{
path: 'Shake/:id',
component: () => import('@/views/ViewCocktail.vue')
},
{
path: "New",
component: () => import('@/views/NewCocktail.vue')
},
{
path: "New/:id",
component: () => import('@/views/EditCocktail.vue')
},
{
path: 'Cocktails',
component: () => import('@/views/Cocktails.vue')
},
{
path: "Apikey",
component: () => import('@/views/ApiKey.vue')
},
{
path: "motiondetector",
component: () => import('@/views/MotionDetector.vue')
}
]
},
{
path: "/:catchAll(.*)",
name: "pageNotFound",
component: () => import('@/views/PageNotFound.vue')
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router

236
src/theme/variables.css Normal file
View File

@ -0,0 +1,236 @@
/* Ionic Variables and Theming. For more info, please see:
http://ionicframework.com/docs/theming/ */
/** Ionic CSS Variables **/
:root {
/** primary **/
--ion-color-primary: #3880ff;
--ion-color-primary-rgb: 56, 128, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3171e0;
--ion-color-primary-tint: #4c8dff;
/** secondary **/
--ion-color-secondary: #3dc2ff;
--ion-color-secondary-rgb: 61, 194, 255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255, 255, 255;
--ion-color-secondary-shade: #36abe0;
--ion-color-secondary-tint: #50c8ff;
/** tertiary **/
--ion-color-tertiary: #5260ff;
--ion-color-tertiary-rgb: 82, 96, 255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
--ion-color-tertiary-shade: #4854e0;
--ion-color-tertiary-tint: #6370ff;
/** success **/
--ion-color-success: #2dd36f;
--ion-color-success-rgb: 45, 211, 111;
--ion-color-success-contrast: #ffffff;
--ion-color-success-contrast-rgb: 255, 255, 255;
--ion-color-success-shade: #28ba62;
--ion-color-success-tint: #42d77d;
/** warning **/
--ion-color-warning: #ffc409;
--ion-color-warning-rgb: 255, 196, 9;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0, 0, 0;
--ion-color-warning-shade: #e0ac08;
--ion-color-warning-tint: #ffca22;
/** danger **/
--ion-color-danger: #eb445a;
--ion-color-danger-rgb: 235, 68, 90;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255, 255, 255;
--ion-color-danger-shade: #cf3c4f;
--ion-color-danger-tint: #ed576b;
/** dark **/
--ion-color-dark: #222428;
--ion-color-dark-rgb: 34, 36, 40;
--ion-color-dark-contrast: #ffffff;
--ion-color-dark-contrast-rgb: 255, 255, 255;
--ion-color-dark-shade: #1e2023;
--ion-color-dark-tint: #383a3e;
/** medium **/
--ion-color-medium: #92949c;
--ion-color-medium-rgb: 146, 148, 156;
--ion-color-medium-contrast: #ffffff;
--ion-color-medium-contrast-rgb: 255, 255, 255;
--ion-color-medium-shade: #808289;
--ion-color-medium-tint: #9d9fa6;
/** light **/
--ion-color-light: #f4f5f8;
--ion-color-light-rgb: 244, 245, 248;
--ion-color-light-contrast: #000000;
--ion-color-light-contrast-rgb: 0, 0, 0;
--ion-color-light-shade: #d7d8da;
--ion-color-light-tint: #f5f6f9;
}
@media (prefers-color-scheme: dark) {
/*
* Dark Colors
* -------------------------------------------
*/
body {
--ion-color-primary: #428cff;
--ion-color-primary-rgb: 66,140,255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255,255,255;
--ion-color-primary-shade: #3a7be0;
--ion-color-primary-tint: #5598ff;
--ion-color-secondary: #50c8ff;
--ion-color-secondary-rgb: 80,200,255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255,255,255;
--ion-color-secondary-shade: #46b0e0;
--ion-color-secondary-tint: #62ceff;
--ion-color-tertiary: #6a64ff;
--ion-color-tertiary-rgb: 106,100,255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255,255,255;
--ion-color-tertiary-shade: #5d58e0;
--ion-color-tertiary-tint: #7974ff;
--ion-color-success: #2fdf75;
--ion-color-success-rgb: 47,223,117;
--ion-color-success-contrast: #000000;
--ion-color-success-contrast-rgb: 0,0,0;
--ion-color-success-shade: #29c467;
--ion-color-success-tint: #44e283;
--ion-color-warning: #ffd534;
--ion-color-warning-rgb: 255,213,52;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0,0,0;
--ion-color-warning-shade: #e0bb2e;
--ion-color-warning-tint: #ffd948;
--ion-color-danger: #ff4961;
--ion-color-danger-rgb: 255,73,97;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255,255,255;
--ion-color-danger-shade: #e04055;
--ion-color-danger-tint: #ff5b71;
--ion-color-dark: #f4f5f8;
--ion-color-dark-rgb: 244,245,248;
--ion-color-dark-contrast: #000000;
--ion-color-dark-contrast-rgb: 0,0,0;
--ion-color-dark-shade: #d7d8da;
--ion-color-dark-tint: #f5f6f9;
--ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152,154,162;
--ion-color-medium-contrast: #000000;
--ion-color-medium-contrast-rgb: 0,0,0;
--ion-color-medium-shade: #86888f;
--ion-color-medium-tint: #a2a4ab;
--ion-color-light: #222428;
--ion-color-light-rgb: 34,36,40;
--ion-color-light-contrast: #ffffff;
--ion-color-light-contrast-rgb: 255,255,255;
--ion-color-light-shade: #1e2023;
--ion-color-light-tint: #383a3e;
}
/*
* iOS Dark Theme
* -------------------------------------------
*/
.ios body {
--ion-background-color: #000000;
--ion-background-color-rgb: 0,0,0;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255,255,255;
--ion-color-step-50: #0d0d0d;
--ion-color-step-100: #1a1a1a;
--ion-color-step-150: #262626;
--ion-color-step-200: #333333;
--ion-color-step-250: #404040;
--ion-color-step-300: #4d4d4d;
--ion-color-step-350: #595959;
--ion-color-step-400: #666666;
--ion-color-step-450: #737373;
--ion-color-step-500: #808080;
--ion-color-step-550: #8c8c8c;
--ion-color-step-600: #999999;
--ion-color-step-650: #a6a6a6;
--ion-color-step-700: #b3b3b3;
--ion-color-step-750: #bfbfbf;
--ion-color-step-800: #cccccc;
--ion-color-step-850: #d9d9d9;
--ion-color-step-900: #e6e6e6;
--ion-color-step-950: #f2f2f2;
--ion-item-background: #000000;
--ion-card-background: #1c1c1d;
}
.ios ion-modal {
--ion-background-color: var(--ion-color-step-100);
--ion-toolbar-background: var(--ion-color-step-150);
--ion-toolbar-border-color: var(--ion-color-step-250);
}
/*
* Material Design Dark Theme
* -------------------------------------------
*/
.md body {
--ion-background-color: #121212;
--ion-background-color-rgb: 18,18,18;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255,255,255;
--ion-border-color: #222222;
--ion-color-step-50: #1e1e1e;
--ion-color-step-100: #2a2a2a;
--ion-color-step-150: #363636;
--ion-color-step-200: #414141;
--ion-color-step-250: #4d4d4d;
--ion-color-step-300: #595959;
--ion-color-step-350: #656565;
--ion-color-step-400: #717171;
--ion-color-step-450: #7d7d7d;
--ion-color-step-500: #898989;
--ion-color-step-550: #949494;
--ion-color-step-600: #a0a0a0;
--ion-color-step-650: #acacac;
--ion-color-step-700: #b8b8b8;
--ion-color-step-750: #c4c4c4;
--ion-color-step-800: #d0d0d0;
--ion-color-step-850: #dbdbdb;
--ion-color-step-900: #e7e7e7;
--ion-color-step-950: #f3f3f3;
--ion-item-background: #1e1e1e;
--ion-toolbar-background: #1f1f1f;
--ion-tab-bar-background: #1f1f1f;
--ion-card-background: #1e1e1e;
}
}

208
src/views/ApiKey.vue Normal file
View File

@ -0,0 +1,208 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>API Key</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">API Key</ion-title>
</ion-toolbar>
</ion-header>
<form v-on:submit.prevent="doSaveKey(apiKey)">
<ion-item>
<ion-label position="fixed">ApiKey: </ion-label>
<ion-input
type="text"
inputmode="text"
minlength="1"
maxlength="255"
name="key"
v-model="apiKey"
required
></ion-input>
</ion-item>
<ion-toolbar>
<ion-button
slot="start"
color="primary"
@click="() => router.push('/tabs/Cocktails')"
>
<ion-icon :icon="arrowBackCircleOutline"></ion-icon>
Back
</ion-button>
<ion-button slot="start" color="danger" @click="doRestoreKey()">
<ion-icon :icon="reloadCircleOutline"></ion-icon>
Restore
</ion-button>
<ion-button slot="end" color="success" type="submit">
<ion-icon :icon="saveOutline"></ion-icon>
Save
</ion-button>
</ion-toolbar>
</form>
<div class="checker">
<ion-button expand="block" color="secondary" @click="doCheckKey()">
<ion-icon :icon="helpCircleOutline"></ion-icon>
Check Key
</ion-button>
<ion-item v-if="apiKey == '1'">
<p>
ApiKey is only test key! Get your own on Patreon
<ion-button href="https://www.patreon.com/thedatadb"
>CocktailDB</ion-button
>
</p>
</ion-item>
</div>
<!--<ion-button expand="block" color="secondary" @click="fetchRandomCocktail()">
<ion-icon :icon="helpCircleOutline"></ion-icon>
fetch
</ion-button>-->
</ion-content>
</ion-page>
</template>
<script>
import { useStorage } from "@/composables/useStorage";
import { useRouter } from "vue-router";
import {
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonLabel,
IonItem,
IonInput,
IonButton,
IonIcon,
actionSheetController,
alertController,
} from "@ionic/vue";
import {
trash,
saveOutline,
refreshOutline,
arrowBackCircleOutline,
helpCircleOutline,
reloadCircleOutline,
} from "ionicons/icons";
export default {
name: "ApiKey",
components: {
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonPage,
IonLabel,
IonItem,
IonInput,
IonButton,
IonIcon,
},
setup() {
const router = useRouter();
const { apiKey, updateApiKey, restoreApiKey, testApiKey, fetchRandomCocktail } = useStorage();
const doCheckKey = async () => {
let data = await testApiKey();
if (data && data != "error") {
const alert = await alertController.create({
header: "Success",
subHeader: "DB connected successfull",
message: "Your API Key is valid and can be used",
buttons: ["OK"],
});
await alert.present();
} else {
const alert = await alertController.create({
header: "Fail",
subHeader: "Db connection failed",
message:
"Your API Key may be invalid or your internet connection is broken",
buttons: ["OK"],
});
await alert.present();
}
};
const doRestoreKey = async () => {
const actionSheet = await actionSheetController.create({
header: "Restore API Key",
buttons: [
{
text: "Restore API Key will remove your current key and set it to the default test key!",
role: "destructive",
icon: trash,
handler: () => {
restoreApiKey();
window.scrollTo({ top: 0, left: 0 });
},
},
{
text: "Cancel",
icon: close,
role: "cancel",
handler: () => {
// Nothing to do, action sheet is automatically closed
window.scrollTo({ top: 0, left: 0 });
},
},
],
});
await actionSheet.present();
};
const doSaveKey = async (key) => {
let data = await updateApiKey(key);
if (data && data != false) {
const alert = await alertController.create({
header: "Success",
message: "Your API Key has been saved",
buttons: ["OK"],
});
await alert.present();
} else {
const alert = await alertController.create({
header: "Fail",
message: "Your API Key coudn't be saved :(",
buttons: ["OK"],
});
await alert.present();
}
};
return {
apiKey,
doSaveKey,
doCheckKey,
doRestoreKey,
fetchRandomCocktail,
trash,
saveOutline,
refreshOutline,
router,
arrowBackCircleOutline,
helpCircleOutline,
reloadCircleOutline,
};
},
};
</script>
<style scoped>
ion-item {
width: 95%;
margin-bottom: 10px;
}
.checker {
margin-top: 50px;
}
</style>

344
src/views/Cocktails.vue Normal file
View File

@ -0,0 +1,344 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Cocktails</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large"
>Cocktaillist
{{ cocktails ? "#" + cocktails.length : "#0" }}</ion-title
>
</ion-toolbar>
</ion-header>
<ion-button
color="success"
expand="full"
@click="() => router.push('/tabs/New')"
>
<ion-icon :icon="addCircleOutline"></ion-icon>
Add new cocktail
</ion-button>
<ion-list v-if="cocktails">
<ion-item-sliding v-for="cocktail in cocktails" :key="cocktail.name">
<ion-item-options side="start">
<ion-item-option @click="doDeleteCocktail(cocktail)" color="danger">
<ion-icon slot="icon-only" :icon="trash"></ion-icon>
</ion-item-option>
<ion-item-option
@click="
() => router.push(`/tabs/New/${cocktails.indexOf(cocktail)}`)
"
color="warning"
>
<ion-icon slot="icon-only" :icon="createOutline"></ion-icon>
</ion-item-option>
</ion-item-options>
<ion-item>
<ion-label>
<h2>
<span
@click="
() =>
router.push(`/tabs/Shake/${cocktails.indexOf(cocktail)}`)
"
>
<ion-icon
class="golden"
v-if="cocktail.favourite"
:icon="star"
></ion-icon>
{{ cocktail.name }}
<ion-icon
class="green"
v-if="cocktail.image"
:icon="camera"
></ion-icon>
</span>
</h2>
<p>by {{ cocktail.author }}</p>
</ion-label>
</ion-item>
<ion-item-options side="end">
<ion-item-option
@click="showShareOptions(cocktail)"
color="secondary"
>
<ion-icon slot="icon-only" :icon="shareSocialOutline"></ion-icon>
</ion-item-option>
<ion-item-option
@click="doFavouriseCocktail(cocktail)"
color="success"
>
<ion-icon slot="icon-only" :icon="star"></ion-icon>
</ion-item-option>
</ion-item-options>
</ion-item-sliding>
</ion-list>
<ion-button
color="primary"
expand="full"
@click="() => router.push('/tabs/Apikey')"
>
<ion-icon :icon="keyOutline"></ion-icon>
Your API Key
</ion-button>
<ion-button color="danger" expand="full" @click="doRestoreCocktails()">
<ion-icon :icon="removeCircleOutline"></ion-icon>
Restore Cocktails
</ion-button>
</ion-content>
</ion-page>
</template>
<script>
import { useStorage } from "@/composables/useStorage";
import { useRouter } from "vue-router";
import { SocialSharing } from "@ionic-native/social-sharing";
import {
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonLabel,
IonList,
IonItem,
IonItemOption,
IonItemOptions,
IonItemSliding,
IonIcon,
IonButton,
actionSheetController,
} from "@ionic/vue";
import {
addCircleOutline,
removeCircleOutline,
star,
trash,
camera,
chevronDownCircleOutline,
shareSocialOutline,
createOutline,
logoFacebook,
logoTwitter,
logoWhatsapp,
logoInstagram,
mailOutline,
callOutline,
eyeOutline,
keyOutline,
} from "ionicons/icons";
import { defineComponent } from "vue";
export default defineComponent({
name: "Cocktails",
components: {
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonPage,
IonLabel,
IonList,
IonItem,
IonItemOption,
IonItemOptions,
IonItemSliding,
IonIcon,
IonButton,
},
setup() {
const { cocktails, favouriseCocktail, removeCocktail, restoreCocktails } =
useStorage();
const router = useRouter();
const socialSharing = SocialSharing;
const publicPath = process.env.BASE_URL;
const doDeleteCocktail = async (cocktail) => {
const actionSheet = await actionSheetController.create({
header: cocktail.name,
buttons: [
{
text: "Delete",
role: "destructive",
icon: trash,
handler: () => {
removeCocktail(cocktail);
},
},
{
text: "Cancel",
icon: close,
role: "cancel",
handler: () => {
// Nothing to do, action sheet is automatically closed
},
},
],
});
await actionSheet.present();
};
const doFavouriseCocktail = async (cocktail) => {
favouriseCocktail(cocktail);
};
const showShareOptions = async (cocktail) => {
//console.log("share ", cocktail.name);
var options = {
message: `Check out this fresh cocktail ${cocktail.name} at CocktailShaker APP`, // not supported on some apps (Facebook, Instagram)
subject: `i found a new cocktail you could be interested in: ${cocktail.name}`, // fi. for email
files: [`${publicPath}img/glasses/${cocktail.glass}.svg`], // an array of filenames either locally or remotely
url: "www.northscorp.de/cocktailshaker",
chooserTitle: "Share " + cocktail.name, // Android only, you can override the default share sheet title
//appPackageName: 'com.apple.social.facebook', // Android only, you can provide id of the App you want to share with
//iPadCoordinates: '0,0,0,0' //IOS only iPadCoordinates for where the popover should be point. Format with x,y,width,height
};
const actionSheet = await actionSheetController.create({
header: "Share Cocktail: " + cocktail.name,
buttons: [
{
text: "Email",
icon: mailOutline,
handler: () => {
socialSharing.shareViaEmail(
options.message + "\n" + options.url,
options.subject
);
},
},
{
text: "Facebook",
icon: logoFacebook,
handler: () => {
socialSharing.shareViaFacebook(
options.message,
options.files[0],
options.url
);
},
},
{
text: "Instagram",
icon: logoInstagram,
handler: () => {
socialSharing.shareViaInstagram(
options.message,
options.files[0]
);
},
},
{
text: "SMS",
icon: callOutline,
handler: () => {
socialSharing.shareViaSMS(options.message);
},
},
{
text: "Twitter",
icon: logoTwitter,
handler: () => {
socialSharing.shareViaTwitter(
options.message,
options.files[0],
options.url
);
},
},
{
text: "Whatsapp",
icon: logoWhatsapp,
handler: () => {
socialSharing.shareViaWhatsApp(
options.message,
options.files[0],
options.url
);
},
},
{
text: "Cancel",
icon: close,
role: "cancel",
handler: () => {
// Nothing to do, action sheet is automatically closed
},
},
],
});
await actionSheet.present();
};
const doRestoreCocktails = async () => {
const actionSheet = await actionSheetController.create({
header: "Cocktail",
buttons: [
{
text: "Restore Cocktails will remove all changes made to the cocktaillist!",
role: "destructive",
icon: trash,
handler: () => {
restoreCocktails();
window.scrollTo({ top: 0, left: 0 });
},
},
{
text: "Cancel",
icon: close,
role: "cancel",
handler: () => {
// Nothing to do, action sheet is automatically closed
window.scrollTo({ top: 0, left: 0 });
},
},
],
});
await actionSheet.present();
};
return {
cocktails,
doDeleteCocktail,
doFavouriseCocktail,
showShareOptions,
doRestoreCocktails,
router,
addCircleOutline,
removeCircleOutline,
star,
trash,
camera,
chevronDownCircleOutline,
shareSocialOutline,
createOutline,
eyeOutline,
keyOutline,
};
},
});
</script>
<style scoped>
ion-list {
padding-bottom: 35px;
margin-bottom: 35px;
}
.golden {
color: gold;
}
.green {
color: limegreen;
}
</style>

252
src/views/EditCocktail.vue Normal file
View File

@ -0,0 +1,252 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>{{ mode }} Cocktail</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">{{ mode }} Cocktail</ion-title>
</ion-toolbar>
</ion-header>
<form v-on:submit.prevent="doEditCocktail(cocktail)">
<ion-item>
<ion-label position="floating">Name</ion-label>
<ion-input
type="text"
inputmode="text"
minlength="3"
maxlength="100"
name="name"
v-model.trim="cocktail.name"
required
></ion-input>
</ion-item>
<ion-item-group>
<ion-item-divider>
<ion-label slot="start" position="fixed">Ingredients</ion-label>
<ion-button slot="end" @click="addIngredientField()">
+ Add
</ion-button>
</ion-item-divider>
<ion-item
v-for="(ingredient, index) in cocktail.ingredients"
:key="index"
v-bind:item="ingredient"
v-bind:index="index"
>
<!--amount-->
<ion-input
placeholder="amount"
type="number"
name="{{ingredient}}-amount"
:value="ingredient.amount"
v-model.number="cocktail.ingredients[index].amount"
max="100"
min="1"
required
></ion-input>
<!--Unit-->
<ion-select
ok-text="Okay"
cancel-text="Dismiss"
name="{{ingredient}}-unit"
:value="ingredient.unit"
:v-model="cocktail.ingredients[index].unit"
required
>
<ion-select-option
:value="unit"
v-for="unit in units"
:key="unit"
>{{ unit }}</ion-select-option
>
</ion-select>
<!--Ingredient-->
<ion-select
ok-text="Okay"
cancel-text="Dismiss"
placeholder="ingredient"
name="{{ingredient}}-ingredient"
:value="ingredient.ingredient"
:v-model="cocktail.ingredients[index].ingredient"
required
>
<ion-select-option
:value="ingred"
v-for="ingred in ingredientList"
:key="ingred"
>{{ ingred.name }}</ion-select-option
>
</ion-select>
<p>{{ingredient.ingredient === cocktail.ingredients[index].ingredient}}</p>
<ion-icon
:icon="trash"
@click="removeIngredient(ingredient)"
></ion-icon>
</ion-item>
</ion-item-group>
<ion-item>
<ion-label position="floating">Directions</ion-label>
<ion-textarea
rows="6"
type="text"
inputmode="text"
minlength="3"
maxlength="250"
name="directions"
v-model="cocktail.directions"
spellcheck="true"
required
></ion-textarea>
</ion-item>
<ion-item>
<ion-label position="fixed">Glass</ion-label>
<ion-select
placeholder="Select glass"
ok-text="Okay"
cancel-text="Dismiss"
name="glass"
v-model="cocktail.glass"
required
>
<ion-select-option
:value="glass"
v-for="glass in glasses"
:key="glass"
>{{ glass }}</ion-select-option
>
</ion-select>
</ion-item>
<ion-toolbar>
<ion-button
slot="start"
color="danger"
@click="() => router.push('/tabs/Cocktails')"
>
<ion-icon :icon="trash"></ion-icon>
Cancel
</ion-button>
<ion-button slot="end" color="success" type="submit">
<!--v-bind:disabled="errors.any()"-->
<ion-icon :icon="saveOutline"></ion-icon>
Save
</ion-button>
</ion-toolbar>
</form>
</ion-content>
</ion-page>
</template>
<script>
import { useStorage } from "@/composables/useStorage";
//import router from "@/router";
import glassJson from "../data/glasses.json";
import ingredientJson from "../data/ingredients.json";
import unitJson from "../data/units.json";
import { useRouter } from "vue-router";
import {
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonLabel,
IonItem,
IonInput,
IonTextarea,
IonSelect,
IonSelectOption,
IonButton,
IonIcon,
IonItemGroup,
IonItemDivider,
} from "@ionic/vue";
import { trash, saveOutline,refreshOutline } from "ionicons/icons";
export default {
name: "Cocktails",
components: {
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonPage,
IonLabel,
IonItem,
IonInput,
IonTextarea,
IonSelect,
IonSelectOption,
IonButton,
IonIcon,
IonItemGroup,
IonItemDivider,
},
methods: {
addIngredientField() {
this.cocktail.ingredients.push({
quantity: 0,
unit: "ml",
ingredient: null,
});
},
removeIngredient(item) {
const index = this.cocktail.ingredients.indexOf(item);
if (index > -1) {
this.cocktail.ingredients.splice(index, 1);
}
}
},
data() {
const { cocktails } = useStorage();
return {
mode: "Edit",
cocktails: cocktails,
ingredientList: ingredientJson,
glasses: glassJson,
units: unitJson,
};
},
computed: {
cocktail(){
let editCocktail = this.cocktails[parseInt(this.$route.params.id, 10)];
//console.log(parseInt(this.$route.params.id, 10), editCocktail)
return editCocktail
},
},
setup() {
const router = useRouter();
//const { editCocktail } = useStorage();
const doEditCocktail = async (cocktail) => {
//console.log("editing: ", cocktail);
cocktail?cocktail:false;
//editCocktail(cocktail);
//router.push("/tabs/Cocktails")
};
return {
doEditCocktail,
trash,
saveOutline,
refreshOutline,
router,
};
},
};
</script>
<style scoped>
ion-item {
width: 95%;
margin-bottom: 10px;
}
</style>

View File

@ -0,0 +1,155 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title> Motion Detector </ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title id="top" size="large"> Motion Detector </ion-title>
</ion-toolbar>
</ion-header>
<!-- View Shake button -->
<div class="container">
<ion-button
color="success"
expand="block"
v-if="platform.indexOf('mobile') > -1"
@click="activateMotionSensor()"
>Activate motion sensor</ion-button
>
<ion-list-header>
<ion-label>Data</ion-label>
</ion-list-header>
<ion-list>
<ion-item>
<ion-label>Platform:</ion-label>
{{ platform }}
</ion-item>
<ion-item> <ion-label>x:</ion-label></ion-item>
<ion-item> <ion-label>y:</ion-label></ion-item>
<ion-item> <ion-label>z:</ion-label></ion-item>
</ion-list>
<ion-item>
{{state.acc}}
</ion-item>
</div>
</ion-content>
</ion-page>
</template>
<script>
import { reactive } from "vue";
import {
getPlatforms,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonItem,
IonList,
IonListHeader,
IonLabel,
IonButton,
} from "@ionic/vue";
//import { Motion } from "@capacitor/motion";
export default {
name: "Shake",
components: {
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonPage,
IonItem,
IonList,
IonListHeader,
IonLabel,
IonButton,
},
data: function () {
return {
platform: getPlatforms(),
};
},
setup() {
const state = reactive({
acc: null,
});
const requestDeviceMotion = () => {
if (window.DeviceMotionEvent == null) {
showError("DeviceMotion is not supported.");
} else if (DeviceMotionEvent.requestPermission) {
DeviceMotionEvent.requestPermission().then(
function (state) {
if (state == "granted") {
createMotionSubscription();
} else {
//console.log("Permission denied by user");
}
},
function (err) {
showError(err);
}
);
} else {
// no need for permission
//console.log("no need for permission");
createMotionSubscription();
}
};
const createMotionSubscription = async () => {
if (window.DeviceOrientationEvent) {
//console.log("start listening");
window.addEventListener(
"deviceorientation",
function (e) {
state.acc = e;
//console.log("Device motion event:", e.acceleration.x);
},
false
);
setTimeout(function () {
//console.log("pause listening");
}, 500);
}
};
const showError = () => {
//console.log("Motion error", error);
};
//createMotionSubscription();
requestDeviceMotion();
return { state, requestDeviceMotion, createMotionSubscription };
},
};
</script>
<style scoped>
.container {
margin: 10px;
padding: 5px;
padding-bottom: 35px;
margin-bottom: 35px;
}
ion-badge {
margin: 0 3px 0 3px;
}
ion-list,
#shorter {
width: 90%;
}
ion-button {
margin: 50px;
}
</style>

258
src/views/NewCocktail.vue Normal file
View File

@ -0,0 +1,258 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>{{ mode }} Cocktail</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">{{ mode }} Cocktail</ion-title>
</ion-toolbar>
</ion-header>
<form v-on:submit.prevent="doAddCocktail(cocktail)">
<ion-item>
<ion-label position="floating">Name</ion-label>
<ion-input
type="text"
inputmode="text"
minlength="3"
maxlength="100"
name="name"
v-model.trim="cocktail.name"
required
></ion-input>
</ion-item>
<ion-item-group>
<ion-item-divider>
<ion-label slot="start" position="fixed">Ingredients</ion-label>
<ion-button slot="end" @click="addIngredientField()">
+ Add
</ion-button>
</ion-item-divider>
<ion-item
v-for="(ingredient, index) in cocktail.ingredients"
:key="index"
v-bind:item="ingredient"
v-bind:index="index"
>
<ion-input
:placeholder="ingredient.amount"
type="number"
name="{{ingredient}}-amount"
v-model.number="cocktail.ingredients[index].amount"
max="100"
min="1"
required
></ion-input>
<ion-select
:value="ingredient.unit"
ok-text="Okay"
cancel-text="Dismiss"
name="{{ingredient}}-unit"
:v-model="cocktail.ingredients[index].unit"
required
>
<ion-select-option
:value="unit"
v-for="unit in units"
:key="unit"
>{{ unit }}</ion-select-option
>
</ion-select>
<ion-select
placeholder="add Ingredient"
ok-text="Okay"
cancel-text="Dismiss"
name="{{ingredient}}-name"
:v-model="cocktail.ingredients[index].ingredient"
required
>
<ion-select-option
:value="ingred"
v-for="ingred in ingredients"
:key="ingred"
>{{ ingred.name }}</ion-select-option
>
</ion-select>
<ion-icon
:icon="trash"
@click="removeIngredient(ingredient)"
></ion-icon>
</ion-item>
</ion-item-group>
<ion-item>
<ion-label position="floating">Directions</ion-label>
<ion-textarea
rows="6"
type="text"
inputmode="text"
minlength="3"
maxlength="250"
name="directions"
v-model="cocktail.directions"
spellcheck="true"
required
></ion-textarea>
</ion-item>
<ion-item>
<ion-label position="fixed">Glass</ion-label>
<ion-select
placeholder="Select glass"
ok-text="Okay"
cancel-text="Dismiss"
name="glass"
v-model="cocktail.glass"
required
>
<ion-select-option
:value="glass"
v-for="glass in glasses"
:key="glass"
>{{ glass }}</ion-select-option
>
</ion-select>
</ion-item>
<ion-toolbar>
<ion-button
slot="start"
color="danger"
@click="() => router.push('/tabs/Cocktails')"
>
<ion-icon :icon="trash"></ion-icon>
Cancel
</ion-button>
<ion-button
slot="secondary"
color="warning"
@click="resetForm()"
>
<ion-icon :icon="refreshOutline"></ion-icon>
Reset
</ion-button>
<ion-button slot="end" color="success" type="submit">
<!--v-bind:disabled="errors.any()"-->
<ion-icon :icon="createOutline"></ion-icon>
Add
</ion-button>
</ion-toolbar>
</form>
</ion-content>
</ion-page>
</template>
<script>
import { useStorage } from "@/composables/useStorage";
//import router from "@/router";
import glassJson from "../data/glasses.json";
import ingredientJson from "../data/ingredients.json";
import unitJson from "../data/units.json";
import { useRouter } from "vue-router";
import {
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonLabel,
IonItem,
IonInput,
IonTextarea,
IonSelect,
IonSelectOption,
IonButton,
IonIcon,
IonItemGroup,
IonItemDivider,
} from "@ionic/vue";
import { trash, createOutline,refreshOutline } from "ionicons/icons";
export default {
name: "Cocktails",
components: {
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonPage,
IonLabel,
IonItem,
IonInput,
IonTextarea,
IonSelect,
IonSelectOption,
IonButton,
IonIcon,
IonItemGroup,
IonItemDivider,
},
methods: {
addIngredientField() {
this.cocktail.ingredients.push({
amount: 0,
unit: "ml",
ingredient: null,
});
},
removeIngredient(item) {
const index = this.cocktail.ingredients.indexOf(item);
if (index > -1) {
this.cocktail.ingredients.splice(index, 1);
}
},
resetForm(){
this.cocktail = {
name: null,
ingredients: [{ amount: 0, unit: "ml", ingredient: null }],
directions: null,
glass: null,
}
}
},
data() {
return {
mode: "New", //"Edit"
cocktail: {
name: null,
ingredients: [{ amount: 0, unit: "ml", ingredient: null }],
directions: null,
glass: null,
},
ingredients: ingredientJson,
glasses: glassJson,
units: unitJson,
};
},
setup() {
const router = useRouter();
const { addCocktail } = useStorage();
const doAddCocktail = async (cocktail) => {
//console.log("adding: ", cocktail.name);
addCocktail(cocktail);
router.push("/tabs/Cocktails")
};
return {
doAddCocktail,
trash,
createOutline,
refreshOutline,
router,
};
},
};
</script>
<style scoped>
ion-item {
width: 95%;
margin-bottom: 10px;
}
</style>

View File

@ -0,0 +1,69 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Error 404</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<container>
<h1>Error 404</h1>
<p>We are sorry, this page does not exist</p>
<br />
<ion-button color="success" expand="block" href="/"
>Back to home</ion-button
>
</container>
</ion-content>
</ion-page>
</template>
<script>
import {
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonButton,
} from "@ionic/vue";
export default {
name: "404",
components: {
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonPage,
IonButton,
},
};
</script>
<style scoped>
container {
text-align: center;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
}
container p {
font-size: 16px;
line-height: 22px;
color: #8c8c8c;
margin: 0;
}
container a {
text-decoration: none;
}
container ion-button {
margin: auto;
width: 70%;
}
</style>

206
src/views/Shake.vue Normal file
View File

@ -0,0 +1,206 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>{{
chosenCocktail ? chosenCocktail.name : "Shake"
}}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title id="top" size="large">
{{ chosenCocktail ? chosenCocktail.name : "Shake" }}
</ion-title>
</ion-toolbar>
</ion-header>
<!-- View Shake button -->
<div class="container" v-if="!chosenCocktail">
<h1 class="ion-text-center">
Shake your phone <br />
to get a Cocktail
</h1>
<ion-button color="success" expand="block" @click="doShake()"
>Start shaking</ion-button
>
<ion-img
class="image-icon"
:src="`${publicPath}img/glasses/Martini.svg`"
alt="CocktailShaker"
></ion-img>
</div>
<!-- Show cocktail -->
<div class="container" v-else>
<div class="images">
<ion-img
class="image-icon"
v-if="!chosenCocktail.image"
:src="`${publicPath}img/glasses/${chosenCocktail.glass}.svg`"
:alt="'Glass: ' + chosenCocktail.glass"
></ion-img>
<ion-img
class="bigimage"
v-else
:src="chosenCocktail.image.webviewPath"
:alt="chosenCocktail.glass"
></ion-img>
</div>
<ion-list-header>
<ion-label>Ingredients</ion-label>
</ion-list-header>
<ion-list>
<ion-item
v-for="ingredient in chosenCocktail.ingredients"
:key="ingredient"
>
<div v-if="ingredient.amount">
{{ ingredient.amount }} {{ ingredient.unit }}
{{ ingredient.ingredient }}
<span class="small">&nbsp;&nbsp; {{ ingredient.label }}</span>
</div>
<div v-else>Spezial: {{ ingredient.special }}</div>
</ion-item>
</ion-list>
<hr />
<ion-list-header>
<ion-label>Directions</ion-label>
</ion-list-header>
<ion-item id="shorter">{{ chosenCocktail.directions }}</ion-item>
<hr />
<div v-if="chosenCocktail.garnish">
<ion-list-header>
<ion-label>Garnish</ion-label>
</ion-list-header>
<ion-item id="shorter">{{ chosenCocktail.garnish }}</ion-item>
</div>
<div class="buttonContainer">
<!--Take Photo -->
<ion-button color="secondary" @click="takePhoto(chosenCocktail)">
<ion-icon :icon="camera"></ion-icon>
</ion-button>
<!--Next Cocktail-->
<ion-button color="success" @click="doShake()"
>Shake again</ion-button
>
</div>
</div>
</ion-content>
</ion-page>
</template>
<script>
import { useStorage } from "@/composables/useStorage";
import {
getPlatforms,
IonPage,
IonIcon,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonItem,
IonList,
IonListHeader,
IonLabel,
IonButton,
IonImg,
} from "@ionic/vue";
import { camera } from "ionicons/icons";
export default {
name: "Shake",
components: {
IonHeader,
IonToolbar,
IonTitle,
IonIcon,
IonContent,
IonPage,
IonItem,
IonList,
IonListHeader,
IonLabel,
IonButton,
IonImg,
},
data: function () {
const { cocktails } = useStorage();
return {
cocktails: cocktails,
chosenCocktail: null,
publicPath: process.env.BASE_URL,
platform: getPlatforms(),
};
},
methods: {
doShake: function () {
const {fetchRandomCocktail} = useStorage();
window.scrollTo({ top: 0, left: 0, behavior: "smooth" });
if(Math.random() > 0.8){
fetchRandomCocktail();
}
this.chosenCocktail =
this.cocktails[Math.floor(Math.random() * this.cocktails.length)];
//console.log(this.chosenCocktail.name);
},
},
setup() {
const { takePhoto } = useStorage();
return {
camera,
takePhoto,
};
},
};
</script>
<style scoped>
.container {
margin: 10px;
padding: 5px;
padding-bottom: 35px;
margin-bottom: 35px;
}
ion-badge {
margin: 0 3px 0 3px;
}
ion-list,
#shorter {
width: 90%;
}
.images {
display: block;
text-align: center;
margin: auto;
}
ion-img.image-icon {
filter: invert(68%) sepia(39%) saturate(476%) hue-rotate(86deg)
brightness(118%) contrast(119%);
height: 60px;
margin: auto;
border: none;
}
ion-img.bigimage {
max-width: 70%;
height: auto;
border: 1px solid white;
padding: 3px;
margin: auto;
}
.small {
font-size: smaller;
color: rgb(173, 173, 173);
}
.buttonContainer {
width: 100%;
text-align: center;
display: inline-block;
}
ion-button {
margin: 10px;
}
</style>

34
src/views/Tabs.vue Normal file
View File

@ -0,0 +1,34 @@
<template>
<ion-page>
<ion-tabs>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="Shake" href="/tabs/shake">
<ion-icon :icon="dice" />
<ion-label>Shake</ion-label>
</ion-tab-button>
<ion-tab-button tab="Cocktails" href="/tabs/cocktails">
<ion-icon :icon="listOutline" />
<ion-label>Cocktails</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
</ion-page>
</template>
<script>
import { IonTabBar, IonTabButton, IonTabs, IonLabel, IonIcon, IonPage } from '@ionic/vue';
import { dice, listOutline } from 'ionicons/icons';
export default {
name: 'Tabs',
components: { IonLabel, IonTabs, IonTabBar, IonTabButton, IonIcon, IonPage },
setup() {
return {
dice,
listOutline
}
}
}
</script>

172
src/views/ViewCocktail.vue Normal file
View File

@ -0,0 +1,172 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>{{
chosenCocktail ? chosenCocktail.name : "View"
}}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title id="top" size="large">
{{ chosenCocktail ? chosenCocktail.name : "View" }}
</ion-title>
</ion-toolbar>
</ion-header>
<!-- View wait -->
<div class="container" v-if="!chosenCocktail">
<h1 class="ion-text-center">Please wait</h1>
</div>
<!-- Show cocktail -->
<div class="container" v-else>
<div class="images">
<ion-img
class="image-icon"
v-if="!chosenCocktail.image"
:src="`${publicPath}img/glasses/${chosenCocktail.glass}.svg`"
:alt="'Glass: ' + chosenCocktail.glass"
></ion-img>
<ion-img
class="bigimage"
v-else
:src="chosenCocktail.image.webviewPath"
:alt="chosenCocktail.glass"
></ion-img>
</div>
<ion-list-header>
<ion-label>Ingredients</ion-label>
</ion-list-header>
<ion-list>
<ion-item
v-for="ingredient in chosenCocktail.ingredients"
:key="ingredient"
>
<div v-if="ingredient.amount">
{{ ingredient.amount }} {{ ingredient.unit }}
{{ ingredient.ingredient }}
<span class="small">&nbsp;&nbsp; {{ ingredient.label }}</span>
</div>
<div v-else>Spezial: {{ ingredient.special }}</div>
</ion-item>
</ion-list>
<hr />
<ion-list-header>
<ion-label>Directions</ion-label>
</ion-list-header>
<ion-item id="shorter">{{ chosenCocktail.directions }}</ion-item>
<hr />
<div v-if="chosenCocktail.garnish">
<ion-list-header>
<ion-label>Garnish</ion-label>
</ion-list-header>
<ion-item id="shorter">{{ chosenCocktail.garnish }}</ion-item>
</div>
<ion-button
color="success"
expand="block"
@click="() => router.push(`/tabs/Shake`)"
>get a new Cocktail</ion-button
>
</div>
</ion-content>
</ion-page>
</template>
<script>
import { useStorage } from "@/composables/useStorage";
import { useRouter } from "vue-router";
import {
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonItem,
IonList,
IonListHeader,
IonLabel,
IonButton,
IonImg,
} from "@ionic/vue";
export default {
name: "Shake",
components: {
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonPage,
IonItem,
IonList,
IonListHeader,
IonLabel,
IonButton,
IonImg,
},
data() {
const { cocktails } = useStorage();
return {
cocktails: cocktails,
publicPath: process.env.BASE_URL
};
},
computed: {
chosenCocktail() {
return this.cocktails[parseInt(this.$route.params.id, 10)];
},
},
setup() {
const router = useRouter();
return {
router
};
},
};
</script>
<style scoped>
.container {
margin: 10px;
padding: 5px;
padding-bottom: 35px;
margin-bottom: 35px;
}
ion-badge {
margin: 0 3px 0 3px;
}
ion-list,
#shorter {
width: 90%;
}
ion-button {
margin: 50px;
}
.images {
display: block;
text-align: center;
margin: auto;
}
ion-img.image-icon {
filter: invert(68%) sepia(39%) saturate(476%) hue-rotate(86deg)
brightness(118%) contrast(119%);
height: 60px;
margin: auto;
border: none;
}
ion-img.bigimage {
max-width: 70%;
height: auto;
border: 1px solid white;
padding: 3px;
margin: auto;
}
.small {
font-size: smaller;
color: rgb(173, 173, 173);
}
</style>

39
tsconfig.json Normal file
View File

@ -0,0 +1,39 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"jest"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.js",
"src/**/*.vue",
"tests/**/*.js",
"tests/**/*.tsx"
, "src/main.js" ],
"exclude": [
"node_modules"
]
}

12075
yarn.lock Normal file

File diff suppressed because it is too large Load Diff