aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMark Powers <mark@marks.kitchen>2024-07-14 16:17:59 -0500
committerMark Powers <mark@marks.kitchen>2024-07-14 16:17:59 -0500
commit0e742a485f3fa7d35d26b05980a293b5760e8418 (patch)
tree97510b5e1979f7e02dbcb17ccbc699c4f97e63f2 /src
Initial commitHEADmaster
Diffstat (limited to 'src')
-rw-r--r--src/App.vue11
-rw-r--r--src/api.js201
-rw-r--r--src/assets/quasar-logo-vertical.svg15
-rw-r--r--src/boot/.gitkeep0
-rw-r--r--src/components/EssentialLink.vue48
-rw-r--r--src/css/app.css2
-rw-r--r--src/layouts/MainLayout.vue92
-rw-r--r--src/pages/BookPage.vue69
-rw-r--r--src/pages/ErrorNotFound.vue31
-rw-r--r--src/pages/FormPage.vue116
-rw-r--r--src/pages/IndexPage.vue174
-rw-r--r--src/pages/WorkoutPage.vue36
-rw-r--r--src/router/index.js30
-rw-r--r--src/router/routes.js22
14 files changed, 847 insertions, 0 deletions
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..38442ee
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,11 @@
+<template>
+ <router-view />
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+ name: 'App'
+})
+</script>
diff --git a/src/api.js b/src/api.js
new file mode 100644
index 0000000..ecd999d
--- /dev/null
+++ b/src/api.js
@@ -0,0 +1,201 @@
+import { Notify } from 'quasar'
+
+var config = {
+ //"api_root": "http://localhost:3000",
+ "api_root": "https://postgrest.marks.kitchen",
+ "username": "mark",
+ "password": "bon@ppetit",
+}
+
+function get_headers(){
+ let headers = new Headers();
+ headers.set('Authorization', 'Basic ' + btoa(config.username + ":" + config.password));
+ headers.set('Content-Type', 'application/json');
+ return headers
+}
+
+async function get_forms(){
+ let res = await fetch(`${config.api_root}/form?order=id.asc`,
+ { headers: get_headers(), }
+ )
+ return await res.json()
+}
+
+async function get_books(completed=false){
+ if(completed){
+ let res = await fetch(`${config.api_root}/book?order=completed.desc`,
+ { headers: get_headers(), }
+ )
+ return await res.json()
+ } else {
+ let res = await fetch(`${config.api_root}/book?completed=eq.false`,
+ { headers: get_headers(), }
+ )
+ return await res.json()
+ }
+}
+
+async function insert(datatype, key, value){
+ let data = {
+ "datatype": datatype,
+ "key": key,
+ "value": value,
+ "created": new Date().toISOString(),
+ }
+ const response = await fetch(`${config.api_root}/datapoint`, {
+ method: "POST",
+ headers: get_headers(),
+ body: JSON.stringify(data),
+ })
+ if(!response.ok){
+ Notify.create({
+ "type": "negative",
+ "message": `Issue submitting ${datatype}`
+ })
+ }
+}
+
+async function submit_data(forms, form_data){
+ console.log(Notify)
+ forms.forEach(form => {
+ if(form_data[form.prompt_id]?.length || form.type === "range"){
+ if(form.type === "multiple_select"){
+ // One row per item selected
+ form_data[form.prompt_id].forEach(o => {
+ insert(form.prompt_id, o, true)
+ })
+ } else {
+ // No key for numeric or text inputs
+ insert(form.prompt_id, undefined, form_data[form.prompt_id])
+ }
+ }
+ })
+ Notify.create({
+ "type": "positive",
+ "message": `Submitted sucessfully.`
+ })
+}
+
+async function edit_book(book_data){
+ let data = {
+ "title": book_data.title,
+ "in_library": book_data.in_library,
+ "lcc": book_data.lcc
+ }
+ const response = await fetch(`${config.api_root}/book?id=eq.${book_data.id}`, {
+ method: "PATCH",
+ headers: get_headers(),
+ body: JSON.stringify(data),
+ })
+ Notify.create({
+ "type": "positive",
+ "message": `Submitted sucessfully.`
+ })
+}
+
+async function submit_book_data(book_form_data){
+ Object.keys(book_form_data).forEach( key => {
+ let obj = book_form_data[key]
+ if(obj.completed){
+ complete_book(key)
+ }
+ if(obj.progress.length > 0){
+ create_book_datapoint(key, obj.progress)
+ }
+ })
+ Notify.create({
+ "type": "positive",
+ "message": `Submitted books sucessfully.`
+ })
+}
+
+async function create_option(form, value){
+ let data = {
+ "extra": form.extra.map( (o) => {return {
+ "id": o.id, "display": o.display,
+ }})
+ }
+ data["extra"].push({
+ "id": value, "display": value,
+ })
+ const response = await fetch(
+ `${config.api_root}/form?id=eq.${form.id}`, {
+ method: "PATCH",
+ headers: get_headers(),
+ body: JSON.stringify(data),
+ })
+}
+
+async function create_book_datapoint(book_id, pages){
+ const response = await fetch(
+ `${config.api_root}/book_datapoint`, {
+ method: "POST",
+ headers: get_headers(),
+ body: JSON.stringify({
+ "created": new Date().toISOString(),
+ "book_id": book_id,
+ "pages": pages,
+ }),
+ })
+}
+
+async function complete_book(book_id){
+ const response = await fetch(
+ `${config.api_root}/book?id=eq.${book_id}`, {
+ method: "PATCH",
+ headers: get_headers(),
+ body: JSON.stringify({
+ "completed": true,
+ }),
+ })
+}
+
+async function create_book(name){
+ const response = await fetch(
+ `${config.api_root}/book`, {
+ method: "POST",
+ headers: get_headers(),
+ body: JSON.stringify({
+ "title": name,
+ "completed": false
+ }),
+ })
+ Notify.create({
+ "type": "positive",
+ "message": `Created book.`
+ })
+}
+
+async function get_book_datapoint(){
+ let res = await fetch(`${config.api_root}/book_datapoint`,
+ { headers: get_headers(), }
+ )
+ return await res.json()
+}
+
+async function create_form(data){
+ const response = await fetch(
+ `${config.api_root}/form`, {
+ method: "POST",
+ headers: get_headers(),
+ body: JSON.stringify(data),
+ })
+ Notify.create({
+ "type": "positive",
+ "message": `Created form.`
+ })
+}
+
+export {
+ get_forms,
+ submit_data,
+ submit_book_data,
+ create_option,
+ get_books,
+ get_book_datapoint,
+ complete_book,
+ create_book_datapoint,
+ create_book,
+ create_form,
+ edit_book,
+}
diff --git a/src/assets/quasar-logo-vertical.svg b/src/assets/quasar-logo-vertical.svg
new file mode 100644
index 0000000..8210831
--- /dev/null
+++ b/src/assets/quasar-logo-vertical.svg
@@ -0,0 +1,15 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 356 360">
+ <path
+ d="M43.4 303.4c0 3.8-2.3 6.3-7.1 6.3h-15v-22h14.4c4.3 0 6.2 2.2 6.2 5.2 0 2.6-1.5 4.4-3.4 5 2.8.4 4.9 2.5 4.9 5.5zm-8-13H24.1v6.9H35c2.1 0 4-1.3 4-3.8 0-2.2-1.3-3.1-3.7-3.1zm5.1 12.6c0-2.3-1.8-3.7-4-3.7H24.2v7.7h11.7c3.4 0 4.6-1.8 4.6-4zm36.3 4v2.7H56v-22h20.6v2.7H58.9v6.8h14.6v2.3H58.9v7.5h17.9zm23-5.8v8.5H97v-8.5l-11-13.4h3.4l8.9 11 8.8-11h3.4l-10.8 13.4zm19.1-1.8V298c0-7.9 5.2-10.7 12.7-10.7 7.5 0 13 2.8 13 10.7v1.4c0 7.9-5.5 10.8-13 10.8s-12.7-3-12.7-10.8zm22.7 0V298c0-5.7-3.9-8-10-8-6 0-9.8 2.3-9.8 8v1.4c0 5.8 3.8 8.1 9.8 8.1 6 0 10-2.3 10-8.1zm37.2-11.6v21.9h-2.9l-15.8-17.9v17.9h-2.8v-22h3l15.6 18v-18h2.9zm37.9 10.2v1.3c0 7.8-5.2 10.4-12.4 10.4H193v-22h11.2c7.2 0 12.4 2.8 12.4 10.3zm-3 0c0-5.3-3.3-7.6-9.4-7.6h-8.4V307h8.4c6 0 9.5-2 9.5-7.7V298zm50.8-7.6h-9.7v19.3h-3v-19.3h-9.7v-2.6h22.4v2.6zm34.4-2.6v21.9h-3v-10.1h-16.8v10h-2.8v-21.8h2.8v9.2H296v-9.2h2.9zm34.9 19.2v2.7h-20.7v-22h20.6v2.7H316v6.8h14.5v2.3H316v7.5h17.8zM24 340.2v7.3h13.9v2.4h-14v9.6H21v-22h20v2.7H24zm41.5 11.4h-9.8v7.9H53v-22h13.3c5.1 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6H66c3.1 0 5.3-1.5 5.3-4.7 0-3.3-2.2-4.1-5.3-4.1H55.7v8.8zm47.9 6.2H89l-2 4.3h-3.2l10.7-22.2H98l10.7 22.2h-3.2l-2-4.3zm-1-2.3l-6.3-13-6 13h12.2zm46.3-15.3v21.9H146v-17.2L135.7 358h-2.1l-10.2-15.6v17h-2.8v-21.8h3l11 16.9 11.3-17h3zm35 19.3v2.6h-20.7v-22h20.6v2.7H166v6.8h14.5v2.3H166v7.6h17.8zm47-19.3l-8.3 22h-3l-7.1-18.6-7 18.6h-3l-8.2-22h3.3L204 356l6.8-18.5h3.4L221 356l6.6-18.5h3.3zm10 11.6v-1.4c0-7.8 5.2-10.7 12.7-10.7 7.6 0 13 2.9 13 10.7v1.4c0 7.9-5.4 10.8-13 10.8-7.5 0-12.7-3-12.7-10.8zm22.8 0v-1.4c0-5.7-4-8-10-8s-9.9 2.3-9.9 8v1.4c0 5.8 3.8 8.2 9.8 8.2 6.1 0 10-2.4 10-8.2zm28.3 2.4h-9.8v7.9h-2.8v-22h13.2c5.2 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6h10.2c3 0 5.2-1.5 5.2-4.7 0-3.3-2.1-4.1-5.2-4.1h-10.2v8.8zm40.3-1.5l-6.8 5.6v6.4h-2.9v-22h2.9v12.3l15.2-12.2h3.7l-9.9 8.1 10.3 13.8h-3.6l-8.9-12z" />
+ <path fill="#050A14"
+ d="M188.4 71.7a10.4 10.4 0 01-20.8 0 10.4 10.4 0 1120.8 0zM224.2 45c-2.2-3.9-5-7.5-8.2-10.7l-12 7c-3.7-3.2-8-5.7-12.6-7.3a49.4 49.4 0 00-9.7 13.9 59 59 0 0140.1 14l7.6-4.4a57 57 0 00-5.2-12.5zM178 125.1c4.5 0 9-.6 13.4-1.7v-14a40 40 0 0012.5-7.2 47.7 47.7 0 00-7.1-15.3 59 59 0 01-32.2 27.7v8.7c4.4 1.2 8.9 1.8 13.4 1.8zM131.8 45c-2.3 4-4 8.1-5.2 12.5l12 7a40 40 0 000 14.4c5.7 1.5 11.3 2 16.9 1.5a59 59 0 01-8-41.7l-7.5-4.3c-3.2 3.2-6 6.7-8.2 10.6z" />
+ <path fill="#00B4FF"
+ d="M224.2 98.4c2.3-3.9 4-8 5.2-12.4l-12-7a40 40 0 000-14.5c-5.7-1.5-11.3-2-16.9-1.5a59 59 0 018 41.7l7.5 4.4c3.2-3.2 6-6.8 8.2-10.7zm-92.4 0c2.2 4 5 7.5 8.2 10.7l12-7a40 40 0 0012.6 7.3c4-4.1 7.3-8.8 9.7-13.8a59 59 0 01-40-14l-7.7 4.4c1.2 4.3 3 8.5 5.2 12.4zm46.2-80c-4.5 0-9 .5-13.4 1.7V34a40 40 0 00-12.5 7.2c1.5 5.7 4 10.8 7.1 15.4a59 59 0 0132.2-27.7V20a53.3 53.3 0 00-13.4-1.8z" />
+ <path fill="#00B4FF"
+ d="M178 9.2a62.6 62.6 0 11-.1 125.2A62.6 62.6 0 01178 9.2m0-9.2a71.7 71.7 0 100 143.5A71.7 71.7 0 00178 0z" />
+ <path fill="#050A14"
+ d="M96.6 212v4.3c-9.2-.8-15.4-5.8-15.4-17.8V180h4.6v18.4c0 8.6 4 12.6 10.8 13.5zm16-31.9v18.4c0 8.9-4.3 12.8-10.9 13.5v4.4c9.2-.7 15.5-5.6 15.5-18v-18.3h-4.7zM62.2 199v-2.2c0-12.7-8.8-17.4-21-17.4-12.1 0-20.7 4.7-20.7 17.4v2.2c0 12.8 8.6 17.6 20.7 17.6 1.5 0 3-.1 4.4-.3l11.8 6.2 2-3.3-8.2-4-6.4-3.1a32 32 0 01-3.6.2c-9.8 0-16-3.9-16-13.3v-2.2c0-9.3 6.2-13.1 16-13.1 9.9 0 16.3 3.8 16.3 13.1v2.2c0 5.3-2.1 8.7-5.6 10.8l4.8 2.4c3.4-2.8 5.5-7 5.5-13.2zM168 215.6h5.1L156 179.7h-4.8l17 36zM143 205l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.8-3.7H143zm133.7 10.7h5.2l-17.3-35.9h-4.8l17 36zm-25-10.7l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.7-3.7h-14.8zm73.8-2.5c6-1.2 9-5.4 9-11.4 0-8-4.5-10.9-12.9-10.9h-21.4v35.5h4.6v-31.3h16.5c5 0 8.5 1.4 8.5 6.7 0 5.2-3.5 7.7-8.5 7.7h-11.4v4.1h10.7l9.3 12.8h5.5l-9.9-13.2zm-117.4 9.9c-9.7 0-14.7-2.5-18.6-6.3l-2.2 3.8c5.1 5 11 6.7 21 6.7 1.6 0 3.1-.1 4.6-.3l-1.9-4h-3zm18.4-7c0-6.4-4.7-8.6-13.8-9.4l-10.1-1c-6.7-.7-9.3-2.2-9.3-5.6 0-2.5 1.4-4 4.6-5l-1.8-3.8c-4.7 1.4-7.5 4.2-7.5 8.9 0 5.2 3.4 8.7 13 9.6l11.3 1.2c6.4.6 8.9 2 8.9 5.4 0 2.7-2.1 4.7-6 5.8l1.8 3.9c5.3-1.6 8.9-4.7 8.9-10zm-20.3-21.9c7.9 0 13.3 1.8 18.1 5.7l1.8-3.9a30 30 0 00-19.6-5.9c-2 0-4 .1-5.7.3l1.9 4 3.5-.2z" />
+ <path fill="#00B4FF"
+ d="M.5 251.9c29.6-.5 59.2-.8 88.8-1l88.7-.3 88.7.3 44.4.4 44.4.6-44.4.6-44.4.4-88.7.3-88.7-.3a7981 7981 0 01-88.8-1z" />
+ <path fill="none" d="M-565.2 324H-252v15.8h-313.2z" />
+</svg> \ No newline at end of file
diff --git a/src/boot/.gitkeep b/src/boot/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/boot/.gitkeep
diff --git a/src/components/EssentialLink.vue b/src/components/EssentialLink.vue
new file mode 100644
index 0000000..0c738cd
--- /dev/null
+++ b/src/components/EssentialLink.vue
@@ -0,0 +1,48 @@
+<template>
+ <q-item
+ clickable
+ tag="a"
+ :href="link"
+ >
+ <q-item-section
+ v-if="icon"
+ avatar
+ >
+ <q-icon :name="icon" />
+ </q-item-section>
+
+ <q-item-section>
+ <q-item-label>{{ title }}</q-item-label>
+ <q-item-label caption>{{ caption }}</q-item-label>
+ </q-item-section>
+ </q-item>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+ name: 'EssentialLink',
+ props: {
+ title: {
+ type: String,
+ required: true
+ },
+
+ caption: {
+ type: String,
+ default: ''
+ },
+
+ link: {
+ type: String,
+ default: '#'
+ },
+
+ icon: {
+ type: String,
+ default: ''
+ }
+ }
+})
+</script>
diff --git a/src/css/app.css b/src/css/app.css
new file mode 100644
index 0000000..6886f8a
--- /dev/null
+++ b/src/css/app.css
@@ -0,0 +1,2 @@
+/* app global css */
+
diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue
new file mode 100644
index 0000000..86c7276
--- /dev/null
+++ b/src/layouts/MainLayout.vue
@@ -0,0 +1,92 @@
+<template>
+ <q-layout view="lHh Lpr lFf">
+ <q-header elevated>
+ <q-toolbar>
+ <q-btn
+ flat
+ dense
+ round
+ icon="menu"
+ aria-label="Menu"
+ @click="toggleLeftDrawer"
+ />
+
+ <q-toolbar-title>
+ Mark's Tracker
+ </q-toolbar-title>
+ </q-toolbar>
+ </q-header>
+
+ <q-drawer
+ v-model="leftDrawerOpen"
+ show-if-above
+ bordered
+ >
+ <q-list>
+ <router-link to="/">
+ <q-item clickable>
+ <q-item-section avatar>
+ <q-icon name="list" />
+ </q-item-section>
+ <q-item-section>
+ <q-item-label>Main</q-item-label>
+ </q-item-section>
+ </q-item>
+ </router-link>
+ <router-link to="/books">
+ <q-item clickable>
+ <q-item-section avatar>
+ <q-icon name="menu_book" />
+ </q-item-section>
+ <q-item-section>
+ <q-item-label>Books</q-item-label>
+ </q-item-section>
+ </q-item>
+ </router-link>
+ <router-link to="/forms">
+ <q-item clickable>
+ <q-item-section avatar>
+ <q-icon name="edit" />
+ </q-item-section>
+ <q-item-section>
+ <q-item-label>Forms</q-item-label>
+ </q-item-section>
+ </q-item>
+ </router-link>
+ <router-link to="/workouts">
+ <q-item clickable>
+ <q-item-section avatar>
+ <q-icon name="directions_bike" />
+ </q-item-section>
+ <q-item-section>
+ <q-item-label>Workouts</q-item-label>
+ </q-item-section>
+ </q-item>
+ </router-link>
+ </q-list>
+ </q-drawer>
+
+ <q-page-container>
+ <router-view />
+ </q-page-container>
+ </q-layout>
+</template>
+
+<script>
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+ name: 'MainLayout',
+
+ setup () {
+ const leftDrawerOpen = ref(false)
+
+ return {
+ leftDrawerOpen,
+ toggleLeftDrawer () {
+ leftDrawerOpen.value = !leftDrawerOpen.value
+ }
+ }
+ }
+})
+</script>
diff --git a/src/pages/BookPage.vue b/src/pages/BookPage.vue
new file mode 100644
index 0000000..3172239
--- /dev/null
+++ b/src/pages/BookPage.vue
@@ -0,0 +1,69 @@
+<template>
+ <q-page class="row q-pa-md q-gutter-md column">
+ <q-card v-for="(book, index) in books" :key="index">
+ <q-card-section>
+ {{ book.title }} - {{ book.in_library }} - {{ book.lcc }}
+ <q-btn label="Edit" color="primary" @click="books_dialog = true; active_book = book" size="large"/>
+ <ul>
+ <li v-for="(datapoint, index2) in book.data" :key="index2">
+ {{ datapoint.created }} -
+ {{ datapoint.pages }}
+ </li>
+ </ul>
+ </q-card-section>
+ </q-card>
+ <q-dialog v-model="books_dialog" persistent width="50%">
+ <q-card>
+ <q-card-section>
+ <q-form @submit.prevent="submit_book">
+ <q-input label="Title" v-model="active_book.title"></q-input>
+ <q-toggle label="In Library" v-model="active_book.in_library"></q-toggle>
+ <q-input label="LCC" v-model="active_book.lcc"></q-input>
+ <q-btn label="Submit" type="submit" color="primary" size="large"/>
+ </q-form>
+ </q-card-section>
+ </q-card>
+ </q-dialog>
+ </q-page>
+</template>
+
+<script>
+import { defineComponent, ref } from 'vue'
+import {
+ get_books,
+ get_book_datapoint,
+ edit_book,
+} from '../api.js'
+
+export default defineComponent({
+ name: 'BookPage',
+ setup(){
+ let books = ref([])
+ get_books().then(json => {
+ books.value = json
+ books.value.forEach(book => {
+ book.data = []
+ })
+ get_book_datapoint().then(json => {
+ json.forEach(datapoint => {
+ let book = books.value.find(book => {
+ return book.id == datapoint.book_id
+ })
+ book?.data.push(datapoint)
+ })
+ })
+ })
+ let books_dialog = ref(false)
+ let active_book = ref(null)
+ return {
+ books: books,
+ books_dialog,
+ active_book,
+ submit_book(){
+ edit_book(active_book.value)
+ books_dialog.value = false
+ }
+ }
+ }
+})
+</script>
diff --git a/src/pages/ErrorNotFound.vue b/src/pages/ErrorNotFound.vue
new file mode 100644
index 0000000..c1c178b
--- /dev/null
+++ b/src/pages/ErrorNotFound.vue
@@ -0,0 +1,31 @@
+<template>
+ <div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center">
+ <div>
+ <div style="font-size: 30vh">
+ 404
+ </div>
+
+ <div class="text-h2" style="opacity:.4">
+ Oops. Nothing here...
+ </div>
+
+ <q-btn
+ class="q-mt-xl"
+ color="white"
+ text-color="blue"
+ unelevated
+ to="/"
+ label="Go Home"
+ no-caps
+ />
+ </div>
+ </div>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+ name: 'ErrorNotFound'
+})
+</script>
diff --git a/src/pages/FormPage.vue b/src/pages/FormPage.vue
new file mode 100644
index 0000000..c3040e2
--- /dev/null
+++ b/src/pages/FormPage.vue
@@ -0,0 +1,116 @@
+<template>
+ <q-page class="row q-pa-md q-gutter-md column">
+ <q-card v-for="(form, index) in forms" :key="index">
+ <q-card-section>
+ <div>
+ {{ form.prompt }}
+ </div>
+ <div>
+ {{ form.type }}
+ </div>
+ <div v-if="form.type == 'multiple_select'">
+ <ul>
+ <li v-for="item in form.extra" :key="item.id">
+ {{ item.id }}
+ </li>
+ <li>
+ <q-form @submit.prevent="form.new_choice" class="row">
+ <q-input
+ label="New choice" type="text"
+ class="col-10"
+ v-model="form.new_choice_value"
+ ></q-input>
+ <q-btn label="Submit" type="submit" color="primary"/>
+ </q-form>
+ </li>
+ </ul>
+ </div>
+ <div v-if="form.type == 'range'">
+ <div>min: {{ form.extra.min }}</div>
+ <div>max: {{ form.extra.max }}</div>
+ </div>
+ </q-card-section>
+ </q-card>
+ <q-card>
+ <q-card-section>
+ <p>New form</p>
+ <q-form class="row" @submit.prevent="submit_new_form">
+ <q-select
+ label="Type"
+ class="col-12"
+ :options="options"
+ v-model="new_form_data.type"
+ ></q-select>
+ <q-input
+ label="Prompt" type="text"
+ class="col-12"
+ v-model="new_form_data.prompt"
+ ></q-input>
+ <q-input
+ label="Prompt Id" type="text"
+ class="col-12"
+ v-model="new_form_data.prompt_id"
+ ></q-input>
+ <q-input v-if="new_form_data.type == 'range'"
+ label="Min" type="number" class="col-12"
+ v-model="new_form_data.extra.min"
+ ></q-input>
+ <q-input v-if="new_form_data.type == 'range'"
+ label="Max" type="number" class="col-12"
+ v-model="new_form_data.extra.max"
+ ></q-input>
+ <q-btn label="Submit" type="submit" color="primary"/>
+ </q-form>
+ </q-card-section>
+ </q-card>
+ </q-page>
+</template>
+
+<script>
+import { defineComponent, ref } from 'vue'
+import {
+ get_forms,
+ create_option,
+ create_form,
+} from '../api.js'
+
+export default defineComponent({
+ name: 'FormPage',
+ setup(){
+ let forms = ref([])
+ get_forms().then(json => {
+ forms.value = json
+ forms.value.forEach(form => {
+ form.new_choice_value = ""
+ form.new_choice = function(){
+ create_option(form, form.new_choice_value)
+ }
+ })
+ })
+ let new_form_data = ref({
+ "extra": {},
+ })
+ return {
+ forms,
+ options: [
+ "multiple_select",
+ "number",
+ "range",
+ "text",
+ ],
+ new_form_data,
+ submit_new_form(){
+ if(new_form_data.value.type && new_form_data.value.prompt && new_form_data.value.prompt_id){
+ if(new_form_data.value.type == 'range' && new_form_data.value.extra.min && new_form_data.value.extra.max) {
+ console.log(new_form_data.value)
+ create_form(new_form_data.value)
+ } else if (new_form_data.value.type != 'range'){
+ new_form_data.value.extra = []
+ create_form(new_form_data.value)
+ }
+ }
+ },
+ }
+ }
+})
+</script>
diff --git a/src/pages/IndexPage.vue b/src/pages/IndexPage.vue
new file mode 100644
index 0000000..b97d68e
--- /dev/null
+++ b/src/pages/IndexPage.vue
@@ -0,0 +1,174 @@
+<template>
+ <q-page class="row q-pa-md q-gutter-md">
+ <div>
+ <q-btn label="Log Books" color="primary" @click="books_dialog = true" size="large"/>
+ </div>
+ <q-dialog v-model="books_dialog" persistent full-width>
+ <q-card>
+ <q-form @submit.prevent="submit_books">
+ <q-card-section
+ v-for="(book, index) in books" :key="index" class="row">
+ <q-input
+ :label="book.title" type="number"
+ class="col-10"
+ v-model="book_form_data[book.id].progress"
+ ></q-input>
+ <q-toggle
+ v-model="book_form_data[book.id].completed"
+ label="Completed?"
+ class="col-2"
+ />
+ </q-card-section>
+ <q-card-section>
+ <div class="row">
+ <q-input
+ label="New book" type="text"
+ class="col-10"
+ v-model="new_book_data.name"
+ ></q-input>
+ <q-btn label="Submit" @click="submit_book()" color="primary" size="large"/>
+ </div>
+ </q-card-section>
+ <q-card-actions align="right" class="text-primary">
+ <q-btn label="Submit" type="submit" color="primary" size="large"/>
+ <q-btn flat label="Cancel" v-close-popup size="large"/>
+ </q-card-actions>
+ </q-form>
+ </q-card>
+ </q-dialog>
+ <q-form class="q-gutter-md col-11" @submit.prevent="submit">
+ <div v-for="(form, index) in forms" :key="index">
+ <template v-if="form.type === 'multiple_select'">
+ <div class="row">
+ <q-select
+ :label="form.prompt"
+ filled
+ v-model="form_data[form.prompt_id]"
+ use-input
+ use-chips
+ multiple
+ input-debounce="0"
+ :options="form.filter_options"
+ @filter="form.filterFn"
+ @new-value="form.createValue"
+ class="col-12"
+ ></q-select>
+ </div>
+ </template>
+ <template v-else-if="form.type === 'number'">
+ <q-input
+ :label="form.prompt"
+ type="number"
+ v-model="form_data[form.prompt_id]"
+ ></q-input>
+ </template>
+ <template v-else-if="form.type === 'range'">
+ <span>{{ form.prompt }}</span>
+ <q-slider
+ :min="form.extra.min"
+ :max="form.extra.max"
+ v-model="form_data[form.prompt_id]"
+ ></q-slider>
+ </template>
+ <template v-else-if="form.type === 'text'">
+ <!-- TODO autofill -->
+ <q-input
+ :label="form.prompt"
+ type="text"
+ v-model="form_data[form.prompt_id]"
+ ></q-input>
+ </template>
+ <template v-else-if="form.type === 'textarea'">
+ <q-input
+ :label="form.prompt"
+ type="text"
+ v-model="form_data[form.prompt_id]"
+ ></q-input>
+ </template>
+ </div>
+ <q-btn label="Submit" type="submit" color="primary" size="large"/>
+ </q-form>
+ </q-page>
+</template>
+
+<script>
+import { defineComponent, ref } from 'vue'
+import {
+ get_forms,
+ get_books,
+ submit_data,
+ submit_book_data,
+ create_option,
+ complete_book,
+ create_book_datapoint,
+ create_book,
+} from '../api.js'
+
+export default defineComponent({
+ name: 'IndexPage',
+ setup() {
+ let forms = ref([])
+ let form_data = ref({})
+ get_forms().then(json => {
+ forms.value = json
+ forms.value.forEach(f => {
+ if(f.type === 'multiple_select'){
+ form_data.value[f.prompt_id] = []
+ let str_options = f.extra.map(o => o.id)
+ f.filter_options = str_options
+ f.filterFn = function(val, update) {
+ update(() => {
+ if (val === '') {
+ f.filter_options = str_options
+ } else {
+ const needle = val.toLowerCase()
+ f.filter_options = str_options.filter(
+ v => v.toLowerCase().indexOf(needle) > -1
+ )
+ }
+ })
+ }
+ f.createValue = function(val, done) {
+ if (val.length > 1) {
+ if (!str_options.includes(val)) {
+ create_option(f, val)
+ done(val, 'add-unique')
+ }
+ }
+ }
+ }
+ })
+ })
+
+ let books = ref([])
+ let book_form_data = ref({})
+ let new_book_data = ref({})
+ get_books().then(json => {
+ books.value = json
+ books.value.forEach( book => {
+ book_form_data.value[book.id] = {
+ "completed": false,
+ "progress": "",
+ }
+ })
+ })
+ let books_dialog = ref(false)
+
+ return {
+ forms, form_data,
+ books, book_form_data, new_book_data,
+ books_dialog,
+ submit_book(){
+ create_book(new_book_data.value.name)
+ },
+ submit(){
+ submit_data(forms.value, form_data.value)
+ },
+ submit_books(){
+ submit_book_data(book_form_data.value)
+ books_dialog.value = false
+ }
+ }
+ }
+})
+</script>
diff --git a/src/pages/WorkoutPage.vue b/src/pages/WorkoutPage.vue
new file mode 100644
index 0000000..8a7f59f
--- /dev/null
+++ b/src/pages/WorkoutPage.vue
@@ -0,0 +1,36 @@
+<template>
+ <q-page class="row q-pa-md q-gutter-md column">
+ <q-card v-for="(workout, index) in history" :key="index">
+ <q-card-section>
+ {{ workout.type }}
+ {{ workout.total_time }} minutes
+ {{ workout.distance }} miles
+ {{ workout.calories }} calories
+ </q-card-section>
+ </q-card>
+ </q-page>
+</template>
+
+<script>
+import { defineComponent, ref } from 'vue'
+import {
+} from '../api.js'
+
+export default defineComponent({
+ name: 'WorkoutPage',
+ setup(){
+ let history = ref([
+ {
+ "created": Date.now(),
+ "type": "stationary bike",
+ "total_time": 29,
+ "distance": 8.0,
+ "calories": 270,
+ },
+ ])
+ return {
+ history
+ }
+ }
+})
+</script>
diff --git a/src/router/index.js b/src/router/index.js
new file mode 100644
index 0000000..ca3cd61
--- /dev/null
+++ b/src/router/index.js
@@ -0,0 +1,30 @@
+import { route } from 'quasar/wrappers'
+import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'
+import routes from './routes'
+
+/*
+ * If not building with SSR mode, you can
+ * directly export the Router instantiation;
+ *
+ * The function below can be async too; either use
+ * async/await or return a Promise which resolves
+ * with the Router instance.
+ */
+
+export default route(function (/* { store, ssrContext } */) {
+ const createHistory = process.env.SERVER
+ ? createMemoryHistory
+ : (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
+
+ const Router = createRouter({
+ scrollBehavior: () => ({ left: 0, top: 0 }),
+ routes,
+
+ // Leave this as is and make changes in quasar.conf.js instead!
+ // quasar.conf.js -> build -> vueRouterMode
+ // quasar.conf.js -> build -> publicPath
+ history: createHistory(process.env.VUE_ROUTER_BASE)
+ })
+
+ return Router
+})
diff --git a/src/router/routes.js b/src/router/routes.js
new file mode 100644
index 0000000..b8500ad
--- /dev/null
+++ b/src/router/routes.js
@@ -0,0 +1,22 @@
+
+const routes = [
+ {
+ path: '/',
+ component: () => import('layouts/MainLayout.vue'),
+ children: [
+ { path: '', component: () => import('pages/IndexPage.vue') },
+ { path: '/books', component: () => import('pages/BookPage.vue') },
+ { path: '/forms', component: () => import('pages/FormPage.vue') },
+ { path: '/workouts', component: () => import('pages/WorkoutPage.vue') },
+ ]
+ },
+
+ // Always leave this as last one,
+ // but you can also remove it
+ {
+ path: '/:catchAll(.*)*',
+ component: () => import('pages/ErrorNotFound.vue')
+ }
+]
+
+export default routes