Version 3 #67
@ -131,29 +131,29 @@ with
|
|||||||
/// JournalRequest is the form of a prayer request returned for the request journal display. It also contains
|
/// JournalRequest is the form of a prayer request returned for the request journal display. It also contains
|
||||||
/// properties that may be filled for history and notes.
|
/// properties that may be filled for history and notes.
|
||||||
[<NoComparison; NoEquality>]
|
[<NoComparison; NoEquality>]
|
||||||
type JournalRequest =
|
type JournalRequest = {
|
||||||
{ /// The ID of the request (just the CUID part)
|
/// The ID of the request (just the CUID part)
|
||||||
requestId : RequestId
|
requestId : RequestId
|
||||||
/// The ID of the user to whom the request belongs
|
/// The ID of the user to whom the request belongs
|
||||||
userId : UserId
|
userId : UserId
|
||||||
/// The current text of the request
|
/// The current text of the request
|
||||||
text : string
|
text : string
|
||||||
/// The last time action was taken on the request
|
/// The last time action was taken on the request
|
||||||
asOf : Instant
|
asOf : Instant
|
||||||
/// The last status for the request
|
/// The last status for the request
|
||||||
lastStatus : RequestAction
|
lastStatus : RequestAction
|
||||||
/// The time that this request should reappear in the user's journal
|
/// The time that this request should reappear in the user's journal
|
||||||
snoozedUntil : Instant
|
snoozedUntil : Instant
|
||||||
/// The time after which this request should reappear in the user's journal by configured recurrence
|
/// The time after which this request should reappear in the user's journal by configured recurrence
|
||||||
showAfter : Instant
|
showAfter : Instant
|
||||||
/// The type of recurrence for this request
|
/// The type of recurrence for this request
|
||||||
recurType : Recurrence
|
recurType : Recurrence
|
||||||
/// How many of the recurrence intervals should occur between appearances in the journal
|
/// How many of the recurrence intervals should occur between appearances in the journal
|
||||||
recurCount : int16
|
recurCount : int16
|
||||||
/// History entries for the request
|
/// History entries for the request
|
||||||
history : History list
|
history : History list
|
||||||
/// Note entries for the request
|
/// Note entries for the request
|
||||||
notes : Note list
|
notes : Note list
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions to manipulate journal requests
|
/// Functions to manipulate journal requests
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
> 1%
|
|
||||||
last 2 versions
|
|
||||||
not ie <= 8
|
|
@ -1,18 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
node: true
|
|
||||||
},
|
|
||||||
'extends': [
|
|
||||||
'plugin:vue/essential',
|
|
||||||
'@vue/standard',
|
|
||||||
'@vue/typescript'
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
|
||||||
},
|
|
||||||
parserOptions: {
|
|
||||||
parser: '@typescript-eslint/parser'
|
|
||||||
}
|
|
||||||
}
|
|
24
src/app/.gitignore
vendored
24
src/app/.gitignore
vendored
@ -1,24 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
node_modules
|
|
||||||
/dist
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
|
|
||||||
# Log files
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.idea
|
|
||||||
.vscode
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw*
|
|
||||||
|
|
||||||
# Auth0 settings
|
|
||||||
src/auth/auth0-variables.*
|
|
@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
autoprefixer: {}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2017 Daniel J. Summers
|
|
||||||
|
|
||||||
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.
|
|
@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
]
|
|
||||||
}
|
|
13749
src/app/package-lock.json
generated
13749
src/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,52 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "my-prayer-journal",
|
|
||||||
"version": "3.0.0",
|
|
||||||
"private": true,
|
|
||||||
"description": "myPrayerJournal - Front End",
|
|
||||||
"author": "Daniel J. Summers <daniel@bitbadger.solutions>",
|
|
||||||
"scripts": {
|
|
||||||
"serve": "vue-cli-service serve --port 8081",
|
|
||||||
"build": "vue-cli-service build --modern",
|
|
||||||
"lint": "vue-cli-service lint",
|
|
||||||
"apistart": "cd ../MyPrayerJournal.Api && dotnet run",
|
|
||||||
"publish": "vue-cli-service build --modern && cd ../MyPrayerJournal.Api && dotnet publish -c Release -r linux-x64 --self-contained false",
|
|
||||||
"vue": "vue-cli-service build --modern && cd ../MyPrayerJournal.Api && dotnet run"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@types/events": "^3.0.0",
|
|
||||||
"@vue/composition-api": "^0.5.0",
|
|
||||||
"auth0-js": "^9.7.3",
|
|
||||||
"axios": "^0.19.0",
|
|
||||||
"core-js": "^3.3.2",
|
|
||||||
"date-fns": "^2.14.0",
|
|
||||||
"vue": "^2.5.15",
|
|
||||||
"vue-material": "^1.0.0-beta-14",
|
|
||||||
"vue-router": "^3.3.2",
|
|
||||||
"vuex": "^3.4.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/auth0-js": "^9.13.1",
|
|
||||||
"@types/node": "^13.13.10",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^2.34.0",
|
|
||||||
"@typescript-eslint/parser": "^2.34.0",
|
|
||||||
"@vue/cli-plugin-babel": "^4.4.1",
|
|
||||||
"@vue/cli-plugin-eslint": "^4.4.1",
|
|
||||||
"@vue/cli-plugin-typescript": "^4.4.1",
|
|
||||||
"@vue/cli-service": "^4.4.1",
|
|
||||||
"@vue/eslint-config-standard": "^5.0.0",
|
|
||||||
"@vue/eslint-config-typescript": "^5.0.0",
|
|
||||||
"eslint": "^6.6.0",
|
|
||||||
"eslint-plugin-import": "^2.18.2",
|
|
||||||
"eslint-plugin-node": "^11.1.0",
|
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
|
||||||
"eslint-plugin-standard": "^4.0.1",
|
|
||||||
"eslint-plugin-vue": "^6.0.1",
|
|
||||||
"node-sass": "^4.14.1",
|
|
||||||
"pug": "^2.0.1",
|
|
||||||
"pug-plain-loader": "^1.0.0",
|
|
||||||
"sass-loader": "^8.0.0",
|
|
||||||
"typescript": "^3.9.5",
|
|
||||||
"vue-template-compiler": "^2.5.17",
|
|
||||||
"webpack-bundle-analyzer": "^3.8.0"
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
@ -1,19 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="preload" as="style">
|
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
|
||||||
<title>myPrayerJournal</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
<strong>We're sorry but newapp doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,218 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
#app.page-container
|
|
||||||
md-app(md-waterfall md-mode='fixed-last' role='application')
|
|
||||||
md-app-toolbar.md-large.md-dense.md-primary
|
|
||||||
md-progress-bar(v-if='progress.visible'
|
|
||||||
:md-mode='progress.mode')
|
|
||||||
.md-no-progress-bar(v-if='!progress.visible')
|
|
||||||
.md-toolbar-row
|
|
||||||
.md-toolbar-section-start
|
|
||||||
router-link(to='/').md-title
|
|
||||||
span(style='font-weight:100;') my
|
|
||||||
span(style='font-weight:400;') Prayer
|
|
||||||
span(style='font-weight:700;') Journal
|
|
||||||
navigation
|
|
||||||
md-app-content
|
|
||||||
md-progress-bar(v-if='progress.visible.value'
|
|
||||||
:md-mode='progress.mode.value')
|
|
||||||
router-view
|
|
||||||
md-snackbar(:md-active.sync='snackbar.visible.value'
|
|
||||||
md-position='center'
|
|
||||||
:md-duration='snackbar.interval.value'
|
|
||||||
ref='snackbar') {{ snackbar.message.value }}
|
|
||||||
footer
|
|
||||||
p.mpj-muted-text.mpj-text-right
|
|
||||||
| myPrayerJournal v{{ version }}
|
|
||||||
br
|
|
||||||
em: small.
|
|
||||||
#[router-link(to='/legal/privacy-policy') Privacy Policy] •
|
|
||||||
#[router-link(to='/legal/terms-of-service') Terms of Service] •
|
|
||||||
#[a(href='https://github.com/bit-badger/myprayerjournal' target='_blank') Developed] and hosted by
|
|
||||||
#[a(href='https://bitbadger.solutions' target='_blank') Bit Badger Solutions]
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Vue from 'vue'
|
|
||||||
import { computed, createComponent, inject, onMounted, provide, ref } from '@vue/composition-api'
|
|
||||||
|
|
||||||
import Navigation from '@/components/common/Navigation.vue'
|
|
||||||
|
|
||||||
import auth from './auth'
|
|
||||||
import router from './router'
|
|
||||||
import store from './store'
|
|
||||||
import { Actions } from './store/types'
|
|
||||||
import { SnackbarProps, ProgressProps } from './types' // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
import { provideAuth } from './plugins/auth'
|
|
||||||
import { provideRouter } from './plugins/router'
|
|
||||||
import { provideStore } from './plugins/store'
|
|
||||||
|
|
||||||
function setupSnackbar (): SnackbarProps {
|
|
||||||
const events = new Vue()
|
|
||||||
const visible = ref(false)
|
|
||||||
const message = ref('')
|
|
||||||
const interval = ref(4000)
|
|
||||||
|
|
||||||
const showSnackbar = (msg: string) => {
|
|
||||||
message.value = msg
|
|
||||||
visible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const showInfo = (msg: string) => {
|
|
||||||
interval.value = 4000
|
|
||||||
showSnackbar(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
const showError = (msg: string) => {
|
|
||||||
interval.value = Infinity
|
|
||||||
showSnackbar(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
events.$on('info', showInfo)
|
|
||||||
events.$on('error', showError)
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
events,
|
|
||||||
visible,
|
|
||||||
message,
|
|
||||||
interval,
|
|
||||||
showSnackbar,
|
|
||||||
showInfo,
|
|
||||||
showError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupProgress (): ProgressProps {
|
|
||||||
const events = new Vue()
|
|
||||||
const visible = ref(false)
|
|
||||||
const mode = ref('query')
|
|
||||||
|
|
||||||
const showProgress = (mod: string) => {
|
|
||||||
mode.value = mod
|
|
||||||
visible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideProgress = () => { visible.value = false }
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
events.$on('show', showProgress)
|
|
||||||
events.$on('done', hideProgress)
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
events,
|
|
||||||
visible,
|
|
||||||
mode,
|
|
||||||
showProgress,
|
|
||||||
hideProgress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const SnackbarSymbol = Symbol('Snackbar events')
|
|
||||||
const ProgressSymbol = Symbol('Progress events')
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
name: 'app',
|
|
||||||
components: { Navigation },
|
|
||||||
setup () {
|
|
||||||
const pkg = require('../package.json')
|
|
||||||
|
|
||||||
provideAuth(auth)
|
|
||||||
provideRouter(router)
|
|
||||||
provideStore(store)
|
|
||||||
|
|
||||||
const version = computed(() =>
|
|
||||||
pkg.version.endsWith('.0')
|
|
||||||
? pkg.version.endsWith('.0.0')
|
|
||||||
? pkg.version.substr(0, pkg.version.length - 4)
|
|
||||||
: pkg.version.substr(0, pkg.version.length - 2)
|
|
||||||
: pkg.version)
|
|
||||||
|
|
||||||
const progress = setupProgress()
|
|
||||||
const snackbar = setupSnackbar()
|
|
||||||
|
|
||||||
onMounted(async () => store.dispatch(Actions.CheckAuthentication))
|
|
||||||
|
|
||||||
provide(SnackbarSymbol, snackbar.events)
|
|
||||||
provide(ProgressSymbol, progress.events)
|
|
||||||
|
|
||||||
return {
|
|
||||||
version,
|
|
||||||
progress,
|
|
||||||
snackbar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export function useSnackbar () {
|
|
||||||
const snackbar = inject(SnackbarSymbol)
|
|
||||||
if (!snackbar) {
|
|
||||||
throw new Error('Snackbar not configured')
|
|
||||||
}
|
|
||||||
return snackbar as SnackbarProps
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useProgress () {
|
|
||||||
const progress = inject(ProgressSymbol)
|
|
||||||
if (!progress) {
|
|
||||||
throw new Error('Progress not configured')
|
|
||||||
}
|
|
||||||
return progress as ProgressProps
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="sass">
|
|
||||||
@import "~vue-material/dist/theme/engine"
|
|
||||||
@include md-register-theme("default", (primary: md-get-palette-color(green, 800), accent: md-get-palette-color(gray, 700)))
|
|
||||||
@import "~vue-material/dist/theme/all"
|
|
||||||
|
|
||||||
html, body
|
|
||||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
|
|
||||||
font-size: 1rem
|
|
||||||
p
|
|
||||||
margin-bottom: 0
|
|
||||||
footer
|
|
||||||
border-top: solid 1px lightgray
|
|
||||||
margin: 1rem -1rem 0
|
|
||||||
padding: 0 1rem
|
|
||||||
footer p
|
|
||||||
margin: 0
|
|
||||||
.mpj-full-page-card
|
|
||||||
font-size: 1rem
|
|
||||||
line-height: 1.25rem
|
|
||||||
.mpj-main-content
|
|
||||||
max-width: 60rem
|
|
||||||
margin: auto
|
|
||||||
.mpj-request-text
|
|
||||||
white-space: pre-line
|
|
||||||
p.mpj-request-text
|
|
||||||
margin-top: 0
|
|
||||||
.mpj-text-center
|
|
||||||
text-align: center
|
|
||||||
.mpj-text-nowrap
|
|
||||||
white-space: nowrap
|
|
||||||
.mpj-text-right
|
|
||||||
text-align: right
|
|
||||||
.mpj-muted-text
|
|
||||||
color: rgba(0, 0, 0, .6)
|
|
||||||
.mpj-valign-top
|
|
||||||
vertical-align: top
|
|
||||||
.mpj-narrow
|
|
||||||
max-width: 40rem
|
|
||||||
margin: auto
|
|
||||||
.mpj-skinny
|
|
||||||
max-width: 20rem
|
|
||||||
margin: auto
|
|
||||||
.mpj-full-width
|
|
||||||
width: 100%
|
|
||||||
.md-toolbar > .md-progress-bar
|
|
||||||
height: 2px
|
|
||||||
width: 100%
|
|
||||||
background-color: rgba(255, 255, 255, .8) !important
|
|
||||||
margin: 0
|
|
||||||
.md-toolbar > .md-no-progress-bar
|
|
||||||
height: 2px
|
|
||||||
width: 100%
|
|
||||||
</style>
|
|
@ -1,100 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
const http = axios.create({
|
|
||||||
baseURL: `${location.protocol}//${location.host}/api/`
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API access for myPrayerJournal
|
|
||||||
*/
|
|
||||||
export default {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the bearer token for all future requests
|
|
||||||
* @param token The token to use to identify the user to the server
|
|
||||||
*/
|
|
||||||
setBearer: (token: string) => { http.defaults.headers.common.Authorization = `Bearer ${token}` },
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the bearer token
|
|
||||||
*/
|
|
||||||
removeBearer: () => delete http.defaults.headers.common.Authorization,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a note for a prayer request
|
|
||||||
* @param requestId The Id of the request to which the note applies
|
|
||||||
* @param notes The notes to be added
|
|
||||||
*/
|
|
||||||
addNote: (requestId: string, notes: string) => http.post(`request/${requestId}/note`, { notes }),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a new prayer request
|
|
||||||
* @param requestText The text of the request to be added
|
|
||||||
* @param recurType The type of recurrence for this request
|
|
||||||
* @param recurCount The number of intervals of recurrence
|
|
||||||
*/
|
|
||||||
addRequest: (requestText: string, recurType: string, recurCount: number) =>
|
|
||||||
http.post('request', { requestText, recurType, recurCount }),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all answered requests, along with the text they had when it was answered
|
|
||||||
*/
|
|
||||||
getAnsweredRequests: () => http.get('requests/answered'),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a prayer request (full; includes all history and notes)
|
|
||||||
* @param requestId The Id of the request to retrieve
|
|
||||||
*/
|
|
||||||
getFullRequest: (requestId: string) => http.get(`request/${requestId}/full`),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get past notes for a prayer request
|
|
||||||
* @param requestId The Id of the request for which notes should be retrieved
|
|
||||||
*/
|
|
||||||
getNotes: (requestId: string) => http.get(`request/${requestId}/notes`),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a prayer request (journal-style; only latest update)
|
|
||||||
* @param requestId The Id of the request to retrieve
|
|
||||||
*/
|
|
||||||
getRequest: (requestId: string) => http.get(`request/${requestId}`),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all prayer requests and their most recent updates
|
|
||||||
*/
|
|
||||||
journal: () => http.get('journal'),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show a request after the given date (used for "show now")
|
|
||||||
* @param requestId The ID of the request which should be shown
|
|
||||||
* @param showAfter The ticks after which the request should be shown
|
|
||||||
*/
|
|
||||||
showRequest: (requestId: string, showAfter: number) => http.patch(`request/${requestId}/show`, { showAfter }),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Snooze a request until the given time
|
|
||||||
* @param requestId The ID of the prayer request to be snoozed
|
|
||||||
* @param until The ticks until which the request should be snoozed
|
|
||||||
*/
|
|
||||||
snoozeRequest: (requestId: string, until: number) => http.patch(`request/${requestId}/snooze`, { until }),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update recurrence for a prayer request
|
|
||||||
* @param requestId The ID of the prayer request for which recurrence is being updated
|
|
||||||
* @param recurType The type of recurrence to set
|
|
||||||
* @param recurCount The number of recurrence intervals to set
|
|
||||||
*/
|
|
||||||
updateRecurrence: (requestId: string, recurType: string, recurCount: number) =>
|
|
||||||
http.patch(`request/${requestId}/recurrence`, { recurType, recurCount }),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a prayer request
|
|
||||||
* @param requestId The ID of the request to be updated
|
|
||||||
* @param status The status of the update
|
|
||||||
* @param updateText The text of the update (optional)
|
|
||||||
*/
|
|
||||||
updateRequest: (requestId: string, status: string, updateText: string) =>
|
|
||||||
http.post(`request/${requestId}/history`, { status, updateText })
|
|
||||||
}
|
|
@ -1,184 +0,0 @@
|
|||||||
import { Store } from 'vuex'
|
|
||||||
import auth0, { Auth0DecodedHash } from 'auth0-js'
|
|
||||||
import { EventEmitter } from 'events'
|
|
||||||
|
|
||||||
import { Mutations, AppState } from '@/store/types'
|
|
||||||
|
|
||||||
import Auth0Config from './auth0-variables'
|
|
||||||
import { Session, Token } from './types'
|
|
||||||
|
|
||||||
// Auth0 web authentication instance to use for our calls
|
|
||||||
const webAuth = new auth0.WebAuth({
|
|
||||||
domain: Auth0Config.domain,
|
|
||||||
clientID: Auth0Config.clientId,
|
|
||||||
redirectUri: Auth0Config.appDomain + Auth0Config.callbackUrl,
|
|
||||||
audience: `https://${Auth0Config.domain}/userinfo`,
|
|
||||||
responseType: 'token id_token',
|
|
||||||
scope: 'openid profile email'
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A class to handle all authentication calls and determinations
|
|
||||||
*/
|
|
||||||
export class AuthService extends EventEmitter {
|
|
||||||
// Local storage key for our session data
|
|
||||||
AUTH_SESSION = 'auth-session'
|
|
||||||
|
|
||||||
// Received and calculated values for our ssesion (initially loaded from local storage if present)
|
|
||||||
session = new Session()
|
|
||||||
|
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
this.refreshSession()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the user log in flow
|
|
||||||
* @param customState Application state to be returned after user has authenticated
|
|
||||||
*/
|
|
||||||
login (customState?: any) {
|
|
||||||
webAuth.authorize({
|
|
||||||
appState: customState
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Promisified parseHash function
|
|
||||||
* @returns A promise that resolves with the parsed hash returned from Auth0
|
|
||||||
*/
|
|
||||||
parseHash (): Promise<Auth0DecodedHash> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
webAuth.parseHash((err, authResult) => {
|
|
||||||
if (err || authResult === null) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
resolve(authResult)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle authentication replies from Auth0
|
|
||||||
* @param store The Vuex store
|
|
||||||
*/
|
|
||||||
async handleAuthentication (store: Store<AppState>) {
|
|
||||||
try {
|
|
||||||
const authResult = await this.parseHash()
|
|
||||||
if (authResult && authResult.accessToken && authResult.idToken) {
|
|
||||||
this.setSession(authResult)
|
|
||||||
store.commit(Mutations.UserLoggedOn, this.session.profile)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err) // eslint-disable-line no-console
|
|
||||||
alert(`Error: ${err.error}. Check the console for further details.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up the session and commit it to local storage
|
|
||||||
* @param authResult The authorization result
|
|
||||||
*/
|
|
||||||
setSession (authResult: Auth0DecodedHash) {
|
|
||||||
this.session.profile = authResult.idTokenPayload
|
|
||||||
this.session.id = new Token(authResult.idToken!, this.session.profile.exp * 1000)
|
|
||||||
this.session.access = new Token(authResult.accessToken!, authResult.expiresIn! * 1000 + Date.now())
|
|
||||||
|
|
||||||
localStorage.setItem(this.AUTH_SESSION, JSON.stringify(this.session))
|
|
||||||
|
|
||||||
this.emit('loginEvent', {
|
|
||||||
loggedIn: true,
|
|
||||||
profile: authResult.idTokenPayload,
|
|
||||||
state: authResult.appState || {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh this instance's session from the one in local storage
|
|
||||||
*/
|
|
||||||
refreshSession () {
|
|
||||||
this.session =
|
|
||||||
localStorage.getItem(this.AUTH_SESSION)
|
|
||||||
? JSON.parse(localStorage.getItem(this.AUTH_SESSION) || '{}')
|
|
||||||
: new Session()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renew authorzation tokens with Auth0
|
|
||||||
* @returns A promise with the parsed hash from the Auth0 response
|
|
||||||
*/
|
|
||||||
renewTokens (): Promise<Auth0DecodedHash> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.refreshSession()
|
|
||||||
if (this.session.id.token !== null) {
|
|
||||||
webAuth.checkSession({}, (err, authResult) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
const result = authResult as Auth0DecodedHash
|
|
||||||
this.setSession(result)
|
|
||||||
resolve(result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
reject(new Error('Not logged in'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log out of myPrayerJournal
|
|
||||||
* @param store The Vuex store
|
|
||||||
*/
|
|
||||||
logout (store: Store<AppState>) {
|
|
||||||
// Clear access token and ID token from local storage
|
|
||||||
localStorage.removeItem(this.AUTH_SESSION)
|
|
||||||
this.refreshSession()
|
|
||||||
|
|
||||||
store.commit(Mutations.UserLoggedOff)
|
|
||||||
|
|
||||||
webAuth.logout({
|
|
||||||
returnTo: `${Auth0Config.appDomain}/`,
|
|
||||||
clientID: Auth0Config.clientId
|
|
||||||
})
|
|
||||||
this.emit('loginEvent', { loggedIn: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the given token is currently valid
|
|
||||||
* @param t The token to be validated
|
|
||||||
* @returns True if the token is valid
|
|
||||||
*/
|
|
||||||
isTokenValid = (t: Token) => t.token !== '' && t.expiry !== 0 && Date.now() < t.expiry
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is there a user authenticated?
|
|
||||||
* @returns True if a user is authenticated
|
|
||||||
*/
|
|
||||||
isAuthenticated () {
|
|
||||||
return this.session && this.session.id && this.isTokenValid(this.session.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is the current access token valid?
|
|
||||||
* @returns True if the user's access token is valid
|
|
||||||
*/
|
|
||||||
isAccessTokenValid () {
|
|
||||||
return this.session && this.session.access && this.isTokenValid(this.session.access)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the user's access token, renewing it if required
|
|
||||||
* @returns A promise that resolves to the user's access token
|
|
||||||
*/
|
|
||||||
async getAccessToken (): Promise<string> {
|
|
||||||
if (this.isAccessTokenValid()) {
|
|
||||||
return this.session.access.token
|
|
||||||
} else {
|
|
||||||
const authResult = await this.renewTokens()
|
|
||||||
return authResult.accessToken!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new AuthService()
|
|
@ -1,21 +0,0 @@
|
|||||||
/** A token and expiration set */
|
|
||||||
export class Token {
|
|
||||||
/**
|
|
||||||
* Create a new token
|
|
||||||
* @param token The actual token
|
|
||||||
* @param expiry The expiration for the token
|
|
||||||
*/
|
|
||||||
constructor (public token: string, public expiry: number) { } // eslint-disable-line no-useless-constructor
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A user's current session */
|
|
||||||
export class Session {
|
|
||||||
/** The user's profile from Auth0 */
|
|
||||||
profile: any = {}
|
|
||||||
|
|
||||||
/** The user's ID token */
|
|
||||||
id = new Token('', 0)
|
|
||||||
|
|
||||||
/** The user's access token */
|
|
||||||
access = new Token('', 0)
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
md-content(role='main').mpj-main-content
|
|
||||||
page-title(title='Welcome!'
|
|
||||||
hideOnPage=true)
|
|
||||||
p
|
|
||||||
p.
|
|
||||||
myPrayerJournal is a place where individuals can record their prayer requests, record that they prayed for them,
|
|
||||||
update them as God moves in the situation, and record a final answer received on that request. It also allows
|
|
||||||
individuals to review their answered prayers.
|
|
||||||
p.
|
|
||||||
This site is open and available to the general public. To get started, simply click the “Log On” link
|
|
||||||
above, and log on with either a Microsoft or Google account. You can also learn more about the site at the
|
|
||||||
“Docs” link, also above.
|
|
||||||
</template>
|
|
@ -1,94 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
md-content(role='main').mpj-main-content-wide
|
|
||||||
page-title(:title='title')
|
|
||||||
p(v-if='isLoadingJournal') Loading your prayer journal...
|
|
||||||
template(v-else)
|
|
||||||
md-empty-state(v-if='journal.length === 0'
|
|
||||||
md-icon='done_all'
|
|
||||||
md-label='No Requests to Show'
|
|
||||||
md-description='You have no requests to be shown; see the “Active” link above for snoozed/deferred requests, and the “Answered” link for answered requests')
|
|
||||||
md-button(:to="{ name: 'EditRequest', params: { id: 'new' } }").md-primary.md-raised Add a New Request
|
|
||||||
template(v-else)
|
|
||||||
.mpj-text-center
|
|
||||||
md-button(:to="{ name: 'EditRequest', params: { id: 'new' } }"
|
|
||||||
role='button').md-raised.md-accent #[md-icon add_box] Add a New Request
|
|
||||||
br
|
|
||||||
.mpj-journal
|
|
||||||
request-card(v-for='request in journal'
|
|
||||||
:key='request.requestId'
|
|
||||||
:request='request')
|
|
||||||
notes-edit
|
|
||||||
snooze-request
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Vue from 'vue'
|
|
||||||
import { computed, createComponent, inject, onBeforeMount, provide } from '@vue/composition-api'
|
|
||||||
import { Store } from 'vuex' // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
import NotesEdit from './request/NotesEdit.vue'
|
|
||||||
import RequestCard from './request/RequestCard.vue'
|
|
||||||
import SnoozeRequest from './request/SnoozeRequest.vue'
|
|
||||||
|
|
||||||
import { Actions, AppState } from '../store/types' // eslint-disable-line no-unused-vars
|
|
||||||
import { useStore } from '../plugins/store'
|
|
||||||
import { useSnackbar, useProgress } from '../App.vue'
|
|
||||||
|
|
||||||
const EventSymbol = Symbol('Journal events')
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
components: {
|
|
||||||
NotesEdit,
|
|
||||||
RequestCard,
|
|
||||||
SnoozeRequest
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
/** The Vuex store */
|
|
||||||
const store = useStore() as Store<AppState>
|
|
||||||
|
|
||||||
/** The title of the page */
|
|
||||||
const title = computed(() => `${store.state.user.given_name}’s Prayer Journal`)
|
|
||||||
|
|
||||||
/** Events to which the journal will respond */
|
|
||||||
const eventBus = new Vue()
|
|
||||||
|
|
||||||
/** Reference to the application's snackbar component */
|
|
||||||
const snackbar = useSnackbar()
|
|
||||||
|
|
||||||
/** Reference to the application's progress bar component */
|
|
||||||
const progress = useProgress()
|
|
||||||
|
|
||||||
/** Provide the event bus for child components */
|
|
||||||
provide(EventSymbol, eventBus)
|
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
|
||||||
await store.dispatch(Actions.LoadJournal, progress)
|
|
||||||
snackbar.events.$emit('info', `Loaded ${store.state.journal.length} prayer requests`)
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
title,
|
|
||||||
journal: store.state.journal,
|
|
||||||
isLoadingJournal: store.state.isLoadingJournal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export function useEvents () {
|
|
||||||
const events = inject(EventSymbol)
|
|
||||||
if (!events) {
|
|
||||||
throw new Error('Event bus not configured')
|
|
||||||
}
|
|
||||||
return events as Vue
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="sass">
|
|
||||||
.mpj-journal
|
|
||||||
display: flex
|
|
||||||
flex-flow: row wrap
|
|
||||||
justify-content: center
|
|
||||||
align-items: flex-start
|
|
||||||
.mpj-dialog-content
|
|
||||||
padding: 0 1rem
|
|
||||||
</style>
|
|
@ -1,50 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { computed, createComponent, createElement, onBeforeUnmount, onMounted, ref } from '@vue/composition-api'
|
|
||||||
import { format, formatDistance } from 'date-fns'
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
props: {
|
|
||||||
tag: {
|
|
||||||
type: String,
|
|
||||||
default: 'span'
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
},
|
|
||||||
interval: {
|
|
||||||
type: Number,
|
|
||||||
default: 10000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup (props) {
|
|
||||||
/** Interval ID for updating relative time */
|
|
||||||
let intervalId: number = 0
|
|
||||||
|
|
||||||
/** The relative time string */
|
|
||||||
const fromNow = ref(formatDistance(props.value, Date.now(), { addSuffix: true }))
|
|
||||||
|
|
||||||
/** The actual date/time (used as the title for the relative time) */
|
|
||||||
const actual = computed(() => format(props.value, 'PPPPp'))
|
|
||||||
|
|
||||||
/** Update the relative time string if it is different */
|
|
||||||
const updateFromNow = () => {
|
|
||||||
const newFromNow = formatDistance(props.value, Date.now(), { addSuffix: true })
|
|
||||||
if (newFromNow !== fromNow.value) fromNow.value = newFromNow
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Refresh the relative time string to keep it accurate */
|
|
||||||
onMounted(() => { intervalId = setInterval(updateFromNow, props.interval) })
|
|
||||||
|
|
||||||
/** Cancel refreshing the time string represented with this component */
|
|
||||||
onBeforeUnmount(() => clearInterval(intervalId))
|
|
||||||
|
|
||||||
return () => createElement(props.tag, {
|
|
||||||
domProps: {
|
|
||||||
title: actual.value,
|
|
||||||
innerText: fromNow.value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,76 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
.md-toolbar-row
|
|
||||||
md-tabs(md-sync-route).md-primary
|
|
||||||
template(v-if='isAuthenticated')
|
|
||||||
md-tab(md-label='Journal'
|
|
||||||
to='/journal')
|
|
||||||
md-tab(md-label='Active'
|
|
||||||
to='/requests/active')
|
|
||||||
md-tab(v-if='hasSnoozed.value'
|
|
||||||
md-label='Snoozed'
|
|
||||||
to='/requests/snoozed')
|
|
||||||
md-tab(md-label='Answered'
|
|
||||||
to='/requests/answered')
|
|
||||||
md-tab(md-label='Log Off'
|
|
||||||
href='/user/log-off'
|
|
||||||
@click.prevent='logOff()')
|
|
||||||
md-tab(md-label='Docs'
|
|
||||||
href='https://docs.prayerjournal.me'
|
|
||||||
@click.prevent='showHelp()')
|
|
||||||
template(v-else)
|
|
||||||
md-tab(md-label='Log On'
|
|
||||||
href='/user/log-on'
|
|
||||||
@click.prevent='logOn()')
|
|
||||||
md-tab(md-label='Docs'
|
|
||||||
href='https://docs.prayerjournal.me'
|
|
||||||
@click.prevent='showHelp()')
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, createComponent } from '@vue/composition-api'
|
|
||||||
import { Store } from 'vuex' // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
import { AppState } from '../../store/types' // eslint-disable-line no-unused-vars
|
|
||||||
import { AuthService } from '../../auth' // eslint-disable-line no-unused-vars
|
|
||||||
import { useAuth } from '../../plugins/auth'
|
|
||||||
import { useRouter } from '../../plugins/router'
|
|
||||||
import { useStore } from '../../plugins/store'
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
setup () {
|
|
||||||
/** The Vuex store */
|
|
||||||
const store = useStore() as Store<AppState>
|
|
||||||
|
|
||||||
/** The auth service */
|
|
||||||
const auth = useAuth() as AuthService
|
|
||||||
|
|
||||||
/** The router for myPrayerJournal */
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
/** Whether the user has any snoozed requests */
|
|
||||||
const hasSnoozed = computed(() =>
|
|
||||||
store.state.isAuthenticated &&
|
|
||||||
Array.isArray(store.state.journal) &&
|
|
||||||
store.state.journal.filter(req => req.snoozedUntil > Date.now()).length > 0)
|
|
||||||
|
|
||||||
/** Log a user on using Auth0 */
|
|
||||||
const logOn = () => auth.login()
|
|
||||||
|
|
||||||
/** Log a user off using Auth0 */
|
|
||||||
const logOff = () => {
|
|
||||||
auth.logout(store)
|
|
||||||
router.push('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Open a new window/tab with help */
|
|
||||||
const showHelp = () => { window.open('https://docs.prayerjournal.me', '_blank') }
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasSnoozed,
|
|
||||||
logOn,
|
|
||||||
logOff,
|
|
||||||
showHelp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,27 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
h1(v-if='!hideOnPage'
|
|
||||||
v-html='title').md-title
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { createComponent, watch, ref } from '@vue/composition-api'
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
props: {
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
hideOnPage: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup (props) {
|
|
||||||
watch(ref(props.title), (title: string, prevTitle: string) => {
|
|
||||||
document.title = `${props.title.replace('’', "'")} « myPrayerJournal`
|
|
||||||
})
|
|
||||||
return { }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,59 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
md-content(role='main').mpj-main-content
|
|
||||||
page-title(title='Privacy Policy'
|
|
||||||
hide-on-page=true)
|
|
||||||
md-card
|
|
||||||
md-card-header
|
|
||||||
.md-title Privacy Policy
|
|
||||||
.md-subhead as of May 21, 2018
|
|
||||||
md-card-content.mpj-full-page-card
|
|
||||||
p.
|
|
||||||
The nature of the service is one where privacy is a must. The items below will help you understand the data we
|
|
||||||
collect, access, and store on your behalf as you use this service.
|
|
||||||
hr
|
|
||||||
h3 Third Party Services
|
|
||||||
p.
|
|
||||||
myPrayerJournal utilizes a third-party authentication and identity provider. You should familiarize yourself
|
|
||||||
with the privacy policy for #[a(href='https://auth0.com/privacy' target='_blank') Auth0], as well as your
|
|
||||||
chosen provider (#[a(href='https://privacy.microsoft.com/en-us/privacystatement' target='_blank') Microsoft] or
|
|
||||||
#[a(href='https://policies.google.com/privacy' target='_blank') Google]).
|
|
||||||
hr
|
|
||||||
h3 What We Collect
|
|
||||||
h4 Identifying Data
|
|
||||||
ul
|
|
||||||
li.
|
|
||||||
The only identifying data myPrayerJournal stores is the subscriber (“sub”) field from the token we
|
|
||||||
receive from Auth0, once you have signed in through their hosted service. All information is associated with
|
|
||||||
you via this field.
|
|
||||||
li.
|
|
||||||
While you are signed in, within your browser, the service has access to your first and last names, along with
|
|
||||||
a URL to the profile picture (provided by your selected identity provider). This information is not
|
|
||||||
transmitted to the server, and is removed when “Log Off” is clicked.
|
|
||||||
h4 User Provided Data
|
|
||||||
ul
|
|
||||||
li.
|
|
||||||
myPrayerJournal stores the information you provide, including the text of prayer requests, updates, and notes;
|
|
||||||
and the date/time when certain actions are taken.
|
|
||||||
hr
|
|
||||||
h3 How Your Data Is Accessed / Secured
|
|
||||||
ul
|
|
||||||
li.
|
|
||||||
Your provided data is returned to you, as required, to display your journal or your answered requests. On the
|
|
||||||
server, it is stored in a controlled-access database.
|
|
||||||
li.
|
|
||||||
Your data is backed up, along with other Bit Badger Solutions hosted systems, in a rolling manner; backups are
|
|
||||||
preserved for the prior 7 days, and backups from the 1st and 15th are preserved for 3 months. These backups
|
|
||||||
are stored in a private cloud data repository.
|
|
||||||
li.
|
|
||||||
The data collected and stored is the absolute minimum necessary for the functionality of the service. There
|
|
||||||
are no plans to “monetize” this service, and storing the minimum amount of information means that
|
|
||||||
the data we have is not interesting to purchasers (or those who may have more nefarious purposes).
|
|
||||||
li Access to servers and backups is strictly controlled and monitored for unauthorized access attempts.
|
|
||||||
hr
|
|
||||||
h3 Removing Your Data
|
|
||||||
p.
|
|
||||||
At any time, you may choose to discontinue using this service. Both Microsoft and Google provide ways to revoke
|
|
||||||
access from this application. However, if you want your data removed from the database, please contact daniel at
|
|
||||||
bitbadger.solutions (via e-mail, replacing at with @) prior to doing so, to ensure we can determine which
|
|
||||||
subscriber ID belongs to you.
|
|
||||||
</template>
|
|
@ -1,40 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
md-content(role='main').mpj-main-content
|
|
||||||
page-title(title='Terms of Service'
|
|
||||||
hide-on-page=true)
|
|
||||||
md-card
|
|
||||||
md-card-header
|
|
||||||
.md-title Terms of Service
|
|
||||||
.md-subhead as of May 21, 2018
|
|
||||||
md-card-content.mpj-full-page-card
|
|
||||||
h3 1. Acceptance of Terms
|
|
||||||
p.
|
|
||||||
By accessing this web site, you are agreeing to be bound by these Terms and Conditions, and that you are
|
|
||||||
responsible to ensure that your use of this site complies with all applicable laws. Your continued use of this
|
|
||||||
site implies your acceptance of these terms.
|
|
||||||
h3 2. Description of Service and Registration
|
|
||||||
p.
|
|
||||||
myPrayerJournal is a service that allows individuals to enter and amend their prayer requests. It requires no
|
|
||||||
registration by itself, but access is granted based on a successful login with an external identity provider.
|
|
||||||
See #[router-link(:to="{ name: 'PrivacyPolicy' }") our privacy policy] for details on how that information is
|
|
||||||
accessed and stored.
|
|
||||||
h3 3. Third Party Services
|
|
||||||
p.
|
|
||||||
This service utilizes a third-party service provider for identity management. Review the terms of service for
|
|
||||||
#[a(href='https://auth0.com/terms' target='_blank') Auth0], as well as those for the selected authorization
|
|
||||||
provider (#[a(href='https://www.microsoft.com/en-us/servicesagreement' target='_blank') Microsoft] or
|
|
||||||
#[a(href='https://policies.google.com/terms' target='_blank') Google]).
|
|
||||||
h3 4. Liability
|
|
||||||
p.
|
|
||||||
This service is provided "as is", and no warranty (express or implied) exists. The service and its developers
|
|
||||||
may not be held liable for any damages that may arise through the use of this service.
|
|
||||||
h3 5. Updates to Terms
|
|
||||||
p.
|
|
||||||
These terms and conditions may be updated at any time, and this service does not have the capability to notify
|
|
||||||
users when these change. The date at the top of the page will be updated when any of the text of these terms is
|
|
||||||
updated.
|
|
||||||
hr
|
|
||||||
p.
|
|
||||||
You may also wish to review our #[router-link(:to="{ name: 'PrivacyPolicy' }") privacy policy] to learn how we
|
|
||||||
handle your data.
|
|
||||||
</template>
|
|
@ -1,65 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
md-content(role='main').mpj-main-content
|
|
||||||
page-title(title='Active Requests'
|
|
||||||
hide-on-page=true)
|
|
||||||
template(v-if='isLoaded.value')
|
|
||||||
md-empty-state(v-if='requests.length === 0'
|
|
||||||
md-icon='sentiment_dissatisfied'
|
|
||||||
md-label='No Active Requests'
|
|
||||||
md-description='Your prayer journal has no active requests')
|
|
||||||
md-button(to='/journal').md-primary.md-raised Return to your journal
|
|
||||||
request-list(v-if='requests.length !== 0'
|
|
||||||
title='Active Requests'
|
|
||||||
:requests='requests')
|
|
||||||
p(v-else) Loading journal...
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { onBeforeMount, createComponent, ref } from '@vue/composition-api'
|
|
||||||
import { Store } from 'vuex' // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
import RequestList from './RequestList.vue'
|
|
||||||
import { useProgress } from '../../App.vue'
|
|
||||||
|
|
||||||
import { Actions, AppState, JournalRequest } from '../../store/types' // eslint-disable-line no-unused-vars
|
|
||||||
import { useStore } from '../../plugins/store'
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
components: {
|
|
||||||
RequestList
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
/** The Vuex store */
|
|
||||||
const store = useStore() as Store<AppState>
|
|
||||||
|
|
||||||
/** The progress bar component instance */
|
|
||||||
const progress = useProgress()
|
|
||||||
|
|
||||||
/** The requests, sorted by the date they will be next shown */
|
|
||||||
let requests: JournalRequest[] = []
|
|
||||||
|
|
||||||
/** Whether all requests have been loaded */
|
|
||||||
const isLoaded = ref(false)
|
|
||||||
|
|
||||||
const ensureJournal = async () => {
|
|
||||||
if (!Array.isArray(store.state.journal)) {
|
|
||||||
isLoaded.value = false
|
|
||||||
await store.dispatch(Actions.LoadJournal, progress)
|
|
||||||
}
|
|
||||||
requests = store.state.journal.sort((a, b) => a.showAfter - b.showAfter)
|
|
||||||
isLoaded.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeMount(async () => { await ensureJournal() })
|
|
||||||
|
|
||||||
// TODO: how do we do this?
|
|
||||||
// this.$on('requestUnsnoozed', ensureJournal)
|
|
||||||
// this.$on('requestNowShown', ensureJournal)
|
|
||||||
|
|
||||||
return {
|
|
||||||
requests,
|
|
||||||
isLoaded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,63 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
md-content(role='main').mpj-main-content
|
|
||||||
page-title(title='Answered Requests'
|
|
||||||
hide-on-page=true)
|
|
||||||
template(v-if='loaded')
|
|
||||||
md-empty-state(v-if='requests.length === 0'
|
|
||||||
md-icon='sentiment_dissatisfied'
|
|
||||||
md-label='No Answered Requests'
|
|
||||||
md-description='Your prayer journal has no answered requests; once you have marked one as “Answered”, it will appear here')
|
|
||||||
request-list(v-if='requests.length !== 0'
|
|
||||||
title='Answered Requests'
|
|
||||||
:requests='requests')
|
|
||||||
p(v-else) Loading answered requests...
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { createComponent, onMounted, ref } from '@vue/composition-api'
|
|
||||||
|
|
||||||
import RequestList from './RequestList.vue'
|
|
||||||
|
|
||||||
import api from '../../api'
|
|
||||||
import { JournalRequest } from '../../store/types' // eslint-disable-line no-unused-vars
|
|
||||||
import { useProgress, useSnackbar } from '../../App.vue'
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
components: {
|
|
||||||
RequestList
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
/** The answered requests */
|
|
||||||
let requests: JournalRequest[] = []
|
|
||||||
|
|
||||||
/** Whether the requests have been loaded */
|
|
||||||
const isLoaded = ref(false)
|
|
||||||
|
|
||||||
/** The snackbar component instance */
|
|
||||||
const snackbar = useSnackbar()
|
|
||||||
|
|
||||||
/** The progress bar instance */
|
|
||||||
const progress = useProgress()
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
progress.events.$emit('show', 'query')
|
|
||||||
try {
|
|
||||||
const reqs = await api.getAnsweredRequests()
|
|
||||||
requests = reqs.data
|
|
||||||
progress.events.$emit('done')
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err) // eslint-disable-line no-console
|
|
||||||
snackbar.events.$emit('error', 'Error loading requests; check console for details')
|
|
||||||
progress.events.$emit('done')
|
|
||||||
} finally {
|
|
||||||
isLoaded.value = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
requests,
|
|
||||||
isLoaded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,224 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
md-content(role='main').mpj-narrow
|
|
||||||
page-title(:title='title.value')
|
|
||||||
md-field
|
|
||||||
label(for='request_text') Prayer Request
|
|
||||||
md-textarea(v-model='form.requestText'
|
|
||||||
@blur='trimText()'
|
|
||||||
md-autogrow
|
|
||||||
autofocus).mpj-full-width
|
|
||||||
br
|
|
||||||
template(v-if='!isNew.value')
|
|
||||||
label Also Mark As
|
|
||||||
br
|
|
||||||
md-radio(v-model='form.status'
|
|
||||||
value='Updated') Updated
|
|
||||||
md-radio(v-model='form.status'
|
|
||||||
value='Prayed') Prayed
|
|
||||||
md-radio(v-model='form.status'
|
|
||||||
value='Answered') Answered
|
|
||||||
br
|
|
||||||
label Recurrence
|
|
||||||
|
|
|
||||||
em.mpj-muted-text After prayer, request reappears...
|
|
||||||
br
|
|
||||||
.md-layout
|
|
||||||
.md-layout-item.md-size-30
|
|
||||||
md-radio(v-model='form.recur.typ'
|
|
||||||
value='Immediate') Immediately
|
|
||||||
.md-layout-item.md-size-20
|
|
||||||
md-radio(v-model='form.recur.typ'
|
|
||||||
value='other') Every...
|
|
||||||
.md-layout-item.md-size-10
|
|
||||||
md-field(md-inline)
|
|
||||||
label Count
|
|
||||||
md-input(v-model='form.recur.count'
|
|
||||||
type='number'
|
|
||||||
:disabled='!showRecurrence.value')
|
|
||||||
.md-layout-item.md-size-20
|
|
||||||
md-field
|
|
||||||
label Interval
|
|
||||||
md-select(v-model='form.recur.other'
|
|
||||||
:disabled='!showRecurrence.value')
|
|
||||||
md-option(value='Hours') hours
|
|
||||||
md-option(value='Days') days
|
|
||||||
md-option(value='Weeks') weeks
|
|
||||||
.mpj-text-right
|
|
||||||
md-button(:disabled='!isValidRecurrence.value'
|
|
||||||
@click.stop='saveRequest()').md-primary.md-raised #[md-icon save] Save
|
|
||||||
md-button(@click.stop='goBack()').md-raised #[md-icon arrow_back] Cancel
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { createComponent, ref, computed, onMounted } from '@vue/composition-api'
|
|
||||||
import { Store } from 'vuex' // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
import { Actions, AppState, AddRequestAction, UpdateRequestAction } from '../../store/types' // eslint-disable-line no-unused-vars
|
|
||||||
import { useProgress, useSnackbar } from '../../App.vue'
|
|
||||||
import { useStore } from '../../plugins/store'
|
|
||||||
import { useRouter } from '../../plugins/router'
|
|
||||||
|
|
||||||
/** The recurrence settings for the request */
|
|
||||||
class RecurrenceForm {
|
|
||||||
/** The type of recurrence */
|
|
||||||
typ = 'Immediate'
|
|
||||||
|
|
||||||
/** The type of recurrence (other than immediate) */
|
|
||||||
other = ''
|
|
||||||
|
|
||||||
/** The count of non-immediate intervals */
|
|
||||||
count = ''
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The recurrence represented by the given form
|
|
||||||
* @param x The recurrence form
|
|
||||||
*/
|
|
||||||
static recurrence = (x: RecurrenceForm) => x.typ === 'Immediate' ? 'Immediate' : x.other
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The interval represented by the given form
|
|
||||||
* @param x The recurrence form
|
|
||||||
*/
|
|
||||||
static interval = (x: RecurrenceForm) => x.typ === 'Immediate' ? 0 : Number.parseInt(x.count)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The form for editing the request */
|
|
||||||
class EditForm {
|
|
||||||
/** The ID of the request */
|
|
||||||
requestId = ''
|
|
||||||
|
|
||||||
/** The text of the request */
|
|
||||||
requestText = ''
|
|
||||||
|
|
||||||
/** The status associated with this update */
|
|
||||||
status = 'Updated'
|
|
||||||
|
|
||||||
/** The recurrence for the request */
|
|
||||||
recur = new RecurrenceForm()
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
props: {
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup (props) {
|
|
||||||
/** The Vuex store */
|
|
||||||
const store = useStore() as Store<AppState>
|
|
||||||
|
|
||||||
/** The snackbar component properties */
|
|
||||||
const snackbar = useSnackbar()
|
|
||||||
|
|
||||||
/** The progress bar component properties */
|
|
||||||
const progress = useProgress()
|
|
||||||
|
|
||||||
/** The application router */
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
/** The page title */
|
|
||||||
const title = ref('Edit Prayer Request')
|
|
||||||
|
|
||||||
/** Whether this is a new request */
|
|
||||||
const isNew = ref(false)
|
|
||||||
|
|
||||||
/** The input form */
|
|
||||||
const form = new EditForm()
|
|
||||||
|
|
||||||
/** Is the selected recurrence a valid recurrence? */
|
|
||||||
const isValidRecurrence = computed(() => {
|
|
||||||
if (form.recur.typ === 'Immediate') return true
|
|
||||||
const count = Number.parseInt(form.recur.count)
|
|
||||||
if (isNaN(count) || form.recur.other === '') return false
|
|
||||||
if (form.recur.other === 'Hours' && count > (365 * 24)) return false
|
|
||||||
if (form.recur.other === 'Days' && count > 365) return false
|
|
||||||
if (form.recur.other === 'Weeks' && count > 52) return false
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
/** Whether the recurrence should be shown */
|
|
||||||
const showRecurrence = computed(() => form.recur.typ !== 'Immediate')
|
|
||||||
|
|
||||||
/** Go back 1 in browser history */
|
|
||||||
const goBack = () => { router.go(-1) }
|
|
||||||
|
|
||||||
/** Trim the request text */
|
|
||||||
const trimText = () => { form.requestText = form.requestText.trim() }
|
|
||||||
|
|
||||||
/** Save the edited request */
|
|
||||||
const saveRequest = async () => {
|
|
||||||
if (isNew.value) {
|
|
||||||
const opts: AddRequestAction = {
|
|
||||||
progress,
|
|
||||||
requestText: form.requestText,
|
|
||||||
recurType: RecurrenceForm.recurrence(form.recur),
|
|
||||||
recurCount: RecurrenceForm.interval(form.recur)
|
|
||||||
}
|
|
||||||
await store.dispatch(Actions.AddRequest, opts)
|
|
||||||
snackbar.events.$emit('info', 'New prayer request added')
|
|
||||||
} else {
|
|
||||||
const opts: UpdateRequestAction = {
|
|
||||||
progress,
|
|
||||||
requestId: form.requestId,
|
|
||||||
updateText: form.requestText,
|
|
||||||
status: form.status,
|
|
||||||
recurType: RecurrenceForm.recurrence(form.recur),
|
|
||||||
recurCount: RecurrenceForm.interval(form.recur)
|
|
||||||
}
|
|
||||||
await store.dispatch(Actions.UpdateRequest, opts)
|
|
||||||
if (form.status === 'Answered') {
|
|
||||||
snackbar.events.$emit('info', 'Request updated and removed from active journal')
|
|
||||||
} else {
|
|
||||||
snackbar.events.$emit('info', 'Request updated')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
goBack()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (!Array.isArray(store.state.journal)) {
|
|
||||||
await store.dispatch(Actions.LoadJournal, progress)
|
|
||||||
}
|
|
||||||
if (props.id === 'new') {
|
|
||||||
title.value = 'Add Prayer Request'
|
|
||||||
isNew.value = true
|
|
||||||
form.requestId = ''
|
|
||||||
form.requestText = ''
|
|
||||||
form.status = 'Created'
|
|
||||||
form.recur.typ = 'Immediate'
|
|
||||||
form.recur.other = ''
|
|
||||||
form.recur.count = ''
|
|
||||||
} else {
|
|
||||||
title.value = 'Edit Prayer Request'
|
|
||||||
isNew.value = false
|
|
||||||
const req = store.state.journal.filter(r => r.requestId === props.id)[0]
|
|
||||||
form.requestId = props.id
|
|
||||||
form.requestText = req.text
|
|
||||||
form.status = 'Updated'
|
|
||||||
if (req.recurType === 'Immediate') {
|
|
||||||
form.recur.typ = 'Immediate'
|
|
||||||
form.recur.other = ''
|
|
||||||
form.recur.count = ''
|
|
||||||
} else {
|
|
||||||
form.recur.typ = 'other'
|
|
||||||
form.recur.other = req.recurType
|
|
||||||
form.recur.count = req.recurCount.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
form,
|
|
||||||
goBack,
|
|
||||||
isNew,
|
|
||||||
isValidRecurrence,
|
|
||||||
journal: store.state.journal,
|
|
||||||
saveRequest,
|
|
||||||
showRecurrence,
|
|
||||||
title,
|
|
||||||
trimText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,108 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
md-content(role='main').mpj-main-content
|
|
||||||
page-title(title='Full Prayer Request'
|
|
||||||
hide-on-page=true)
|
|
||||||
md-card(v-if='request')
|
|
||||||
md-card-header
|
|
||||||
.md-title Full Prayer Request
|
|
||||||
.md-subhead
|
|
||||||
span(v-if='isAnswered.value') Answered {{ formatDate(answered) }} (#[date-from-now(:value='answered')]) !{' • '}
|
|
||||||
| Prayed {{ prayedCount }} times • Open {{ openDays }} days
|
|
||||||
md-card-content.mpj-full-page-card
|
|
||||||
p.mpj-request-text {{ lastText }}
|
|
||||||
md-table
|
|
||||||
md-table-row
|
|
||||||
md-table-head Action
|
|
||||||
md-table-head Update / Notes
|
|
||||||
md-table-row(v-for='item in log'
|
|
||||||
:key='item.asOf')
|
|
||||||
md-table-cell.mpj-valign-top {{ item.status }} on #[span.mpj-text-nowrap {{ formatDate(item.asOf) }}]
|
|
||||||
md-table-cell(v-if='item.text').mpj-request-text.mpj-valign-top {{ item.text }}
|
|
||||||
md-table-cell(v-else)
|
|
||||||
p(v-else) Loading request...
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, createComponent, onMounted } from '@vue/composition-api'
|
|
||||||
import { format } from 'date-fns'
|
|
||||||
|
|
||||||
import api from '../../api'
|
|
||||||
import { useProgress } from '../../App.vue'
|
|
||||||
import { HistoryEntry, JournalRequest } from '../../store/types' // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
/** Sort history entries in descending order */
|
|
||||||
const asOfDesc = (a: HistoryEntry, b: HistoryEntry) => b.asOf - a.asOf
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
props: {
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup (props) {
|
|
||||||
/** The progress bar component instance */
|
|
||||||
const progress = useProgress()
|
|
||||||
|
|
||||||
/** The request being displayed */
|
|
||||||
let request = new JournalRequest()
|
|
||||||
|
|
||||||
/** The history entry where the request was marked as answered */
|
|
||||||
const answer = computed(() => request.history.find(hist => hist.status === 'Answered'))
|
|
||||||
|
|
||||||
/** Whether this request is answered */
|
|
||||||
const isAnswered = computed(() => answer.value !== undefined)
|
|
||||||
|
|
||||||
/** The date/time this request was answered */
|
|
||||||
const answered = computed(() => answer.value ? answer.value.asOf : undefined)
|
|
||||||
|
|
||||||
/** The last recorded text for the request */
|
|
||||||
const lastText = computed(() => request.history.filter(hist => hist.text).sort(asOfDesc)[0].text)
|
|
||||||
|
|
||||||
/** The history log including notes (and excluding the final entry for answered requests) */
|
|
||||||
const log = computed(() => {
|
|
||||||
const allHistory = (request.notes || [])
|
|
||||||
.map(note => ({ asOf: note.asOf, text: note.notes, status: 'Notes' } as HistoryEntry))
|
|
||||||
.concat(request.history)
|
|
||||||
.sort(asOfDesc)
|
|
||||||
// Skip the first entry for answered requests; that info is already displayed
|
|
||||||
return isAnswered.value ? allHistory.slice(1) : allHistory
|
|
||||||
})
|
|
||||||
|
|
||||||
/** The number of days this request [was|has been] open */
|
|
||||||
const openDays = computed(() => {
|
|
||||||
const asOf = answered.value ? answered.value : Date.now()
|
|
||||||
return Math.floor(
|
|
||||||
(asOf - request.history.filter(hist => hist.status === 'Created')[0].asOf) / 1000 / 60 / 60 / 24)
|
|
||||||
})
|
|
||||||
|
|
||||||
/** How many times this request has been prayed for */
|
|
||||||
const prayedCount = computed(() => request.history.filter(hist => hist.status === 'Prayed').length)
|
|
||||||
|
|
||||||
/** Format a date */
|
|
||||||
const formatDate = (asOf: number) => format(asOf, 'PPP')
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
progress.events.$emit('show', 'indeterminate')
|
|
||||||
try {
|
|
||||||
const req = await api.getFullRequest(props.id)
|
|
||||||
request = req.data as JournalRequest
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e) // eslint-disable-line no-console
|
|
||||||
} finally {
|
|
||||||
progress.events.$emit('done')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
answered,
|
|
||||||
formatDate,
|
|
||||||
isAnswered,
|
|
||||||
lastText,
|
|
||||||
log,
|
|
||||||
openDays,
|
|
||||||
prayedCount,
|
|
||||||
request
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,155 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
md-dialog(:md-active.sync='notesVisible').mpj-note-dialog
|
|
||||||
md-dialog-title Add Notes to Prayer Request
|
|
||||||
md-content.mpj-dialog-content
|
|
||||||
md-field
|
|
||||||
label Notes
|
|
||||||
md-textarea(v-model='form.notes'
|
|
||||||
md-autogrow
|
|
||||||
@blur='trimText()')
|
|
||||||
md-dialog-actions
|
|
||||||
md-button(@click='saveNotes()').md-primary #[md-icon save] Save
|
|
||||||
md-button(@click='closeDialog()') #[md-icon undo] Cancel
|
|
||||||
md-dialog-content(md-scrollbar='true').mpj-dialog-content
|
|
||||||
div(v-if='hasPriorNotes.value')
|
|
||||||
p.mpj-text-center: strong Prior Notes for This Request
|
|
||||||
.mpj-note-list
|
|
||||||
p(v-for='note in priorNotes'
|
|
||||||
:key='note.asOf')
|
|
||||||
small.mpj-muted-text: date-from-now(:value='note.asOf')
|
|
||||||
br
|
|
||||||
span.mpj-request-text {{ note.notes }}
|
|
||||||
div(v-else-if='noPriorNotes.value').mpj-text-center.mpj-muted-text There are no prior notes for this request
|
|
||||||
div(v-else).mpj-text-center
|
|
||||||
hr
|
|
||||||
md-button(@click='loadNotes()') #[md-icon cloud_download] Load Prior Notes
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, createComponent, ref } from '@vue/composition-api'
|
|
||||||
|
|
||||||
import api from '../../api'
|
|
||||||
|
|
||||||
import { useEvents } from '../Journal.vue'
|
|
||||||
import { NotesEntry, JournalRequest } from '../../store/types' // eslint-disable-line no-unused-vars
|
|
||||||
import { useProgress, useSnackbar } from '../../App.vue'
|
|
||||||
|
|
||||||
/** The input form for the notes dialog */
|
|
||||||
class NotesForm {
|
|
||||||
/** The ID of the request */
|
|
||||||
requestId = ''
|
|
||||||
/** The actual notes */
|
|
||||||
notes = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The prior notes for this request */
|
|
||||||
class PriorNotes {
|
|
||||||
/** The prior notes */
|
|
||||||
notes: NotesEntry[] = []
|
|
||||||
/** Have the prior notes been loaded? */
|
|
||||||
isLoaded = false
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
setup () {
|
|
||||||
/** The event bus for the journal page */
|
|
||||||
const events = useEvents()
|
|
||||||
|
|
||||||
/** The snackbar component properties */
|
|
||||||
const snackbar = useSnackbar()
|
|
||||||
|
|
||||||
/** The progress bar component properties */
|
|
||||||
const progress = useProgress()
|
|
||||||
|
|
||||||
/** Is this dialog visible? */
|
|
||||||
const notesVisible = ref(false)
|
|
||||||
|
|
||||||
/** The input form */
|
|
||||||
const form = new NotesForm()
|
|
||||||
|
|
||||||
/** The prior notes */
|
|
||||||
const prior = new PriorNotes()
|
|
||||||
|
|
||||||
/** Are there prior notes? */
|
|
||||||
const hasPriorNotes = computed(() => prior.isLoaded && prior.notes.length > 0)
|
|
||||||
|
|
||||||
/** Are there no prior notes? */
|
|
||||||
const noPriorNotes = computed(() => prior.isLoaded && prior.notes.length === 0)
|
|
||||||
|
|
||||||
/** Close this dialog */
|
|
||||||
const closeDialog = () => {
|
|
||||||
form.requestId = ''
|
|
||||||
form.notes = ''
|
|
||||||
prior.notes = []
|
|
||||||
prior.isLoaded = false
|
|
||||||
notesVisible.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Load the notes for this request */
|
|
||||||
const loadNotes = async () => {
|
|
||||||
progress.events.$emit('show', 'indeterminate')
|
|
||||||
try {
|
|
||||||
const notes = await api.getNotes(form.requestId)
|
|
||||||
prior.notes = (notes.data as NotesEntry[]).sort((a, b) => b.asOf - a.asOf)
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e) // eslint-disable-line no-console
|
|
||||||
} finally {
|
|
||||||
progress.events.$emit('done')
|
|
||||||
prior.isLoaded = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Open this dialog */
|
|
||||||
const openDialog = (request: JournalRequest) => {
|
|
||||||
form.requestId = request.requestId
|
|
||||||
notesVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Save the notes entered on this dialog */
|
|
||||||
const saveNotes = async () => {
|
|
||||||
progress.events.$emit('show', 'indeterminate')
|
|
||||||
try {
|
|
||||||
await api.addNote(form.requestId, form.notes)
|
|
||||||
snackbar.events.$emit('info', 'Added notes')
|
|
||||||
closeDialog()
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e) // eslint-disable-line no-console
|
|
||||||
} finally {
|
|
||||||
progress.events.$emit('done')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Trim the note text */
|
|
||||||
const trimText = () => { form.notes = form.notes.trim() }
|
|
||||||
|
|
||||||
events.$on('notes', openDialog)
|
|
||||||
|
|
||||||
return {
|
|
||||||
closeDialog,
|
|
||||||
form,
|
|
||||||
hasPriorNotes,
|
|
||||||
loadNotes,
|
|
||||||
noPriorNotes,
|
|
||||||
notesVisible,
|
|
||||||
openDialog,
|
|
||||||
prior,
|
|
||||||
saveNotes,
|
|
||||||
trimText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="sass">
|
|
||||||
.mpj-note-dialog
|
|
||||||
width: 40rem
|
|
||||||
padding-bottom: 1.5rem
|
|
||||||
@media screen and (max-width: 40rem)
|
|
||||||
@media screen and (max-width: 20rem)
|
|
||||||
.mpj-note-dialog
|
|
||||||
width: 100%
|
|
||||||
.mpj-note-dialog
|
|
||||||
width: 20rem
|
|
||||||
.mpj-note-list p
|
|
||||||
border-top: dotted 1px lightgray
|
|
||||||
</style>
|
|
@ -1,108 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
md-card(v-if='shouldDisplay'
|
|
||||||
md-with-hover).mpj-request-card
|
|
||||||
md-card-actions(md-alignment='space-between')
|
|
||||||
md-button(@click='markPrayed()').md-icon-button.md-raised.md-primary
|
|
||||||
md-icon done
|
|
||||||
md-tooltip(md-direction='top'
|
|
||||||
md-delay=1000) Mark as Prayed
|
|
||||||
span
|
|
||||||
md-button(@click.stop='showEdit()').md-icon-button.md-raised
|
|
||||||
md-icon edit
|
|
||||||
md-tooltip(md-direction='top'
|
|
||||||
md-delay=1000) Edit Request
|
|
||||||
md-button(@click.stop='showNotes()').md-icon-button.md-raised
|
|
||||||
md-icon comment
|
|
||||||
md-tooltip(md-direction='top'
|
|
||||||
md-delay=1000) Add Notes
|
|
||||||
md-button(@click.stop='snooze()').md-icon-button.md-raised
|
|
||||||
md-icon schedule
|
|
||||||
md-tooltip(md-direction='top'
|
|
||||||
md-delay=1000) Snooze Request
|
|
||||||
md-card-content
|
|
||||||
p.mpj-request-text {{ request.text }}
|
|
||||||
p.mpj-text-right: small.mpj-muted-text: em (last activity #[date-from-now(:value='request.asOf')])
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { createComponent, computed } from '@vue/composition-api'
|
|
||||||
|
|
||||||
import { Actions, JournalRequest, UpdateRequestAction } from '../../store/types' // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
import { useEvents } from '../Journal.vue'
|
|
||||||
import { useStore } from '../../plugins/store'
|
|
||||||
import { useProgress, useSnackbar } from '../../App.vue'
|
|
||||||
import { useRouter } from '../../plugins/router'
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
props: {
|
|
||||||
request: {
|
|
||||||
type: JournalRequest,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup (props) {
|
|
||||||
/** The Vuex store */
|
|
||||||
const store = useStore()
|
|
||||||
|
|
||||||
/** The application router */
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
/** The progress bar component properties */
|
|
||||||
const progress = useProgress()
|
|
||||||
|
|
||||||
/** The snackbar component properties */
|
|
||||||
const snackbar = useSnackbar()
|
|
||||||
|
|
||||||
/** The journal event bus */
|
|
||||||
const events = useEvents()
|
|
||||||
|
|
||||||
/** Should this request be displayed? */
|
|
||||||
const shouldDisplay = computed(() => {
|
|
||||||
const now = Date.now()
|
|
||||||
return Math.max(now, props.request.showAfter, props.request.snoozedUntil) === now
|
|
||||||
})
|
|
||||||
|
|
||||||
/** Mark the request as prayed */
|
|
||||||
const markPrayed = async () => {
|
|
||||||
const opts: UpdateRequestAction = {
|
|
||||||
progress,
|
|
||||||
requestId: props.request.requestId,
|
|
||||||
status: 'Prayed',
|
|
||||||
updateText: '',
|
|
||||||
recurType: '',
|
|
||||||
recurCount: 0
|
|
||||||
}
|
|
||||||
await store.dispatch(Actions.UpdateRequest, opts)
|
|
||||||
snackbar.events.$emit('info', 'Request marked as prayed')
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Show the edit page for this request */
|
|
||||||
const showEdit = () => { router.push({ name: 'EditRequest', params: { id: props.request.requestId } }) }
|
|
||||||
|
|
||||||
/** Show the request notes dialog */
|
|
||||||
const showNotes = () => events.$emit('notes', props.request)
|
|
||||||
|
|
||||||
/** Show the snooze request dialog */
|
|
||||||
const snooze = () => events.$emit('snooze', props.request.requestId)
|
|
||||||
|
|
||||||
return {
|
|
||||||
markPrayed,
|
|
||||||
request: props.request,
|
|
||||||
shouldDisplay,
|
|
||||||
showEdit,
|
|
||||||
showNotes,
|
|
||||||
snooze
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="sass">
|
|
||||||
.mpj-request-card
|
|
||||||
width: 20rem
|
|
||||||
margin-bottom: 1rem
|
|
||||||
@media screen and (max-width: 20rem)
|
|
||||||
.mpj-request-card
|
|
||||||
width: 100%
|
|
||||||
</style>
|
|
@ -1,44 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
md-table(md-card)
|
|
||||||
md-table-toolbar
|
|
||||||
h1.md-title {{ title }}
|
|
||||||
md-table-row
|
|
||||||
md-table-head Actions
|
|
||||||
md-table-head Request
|
|
||||||
request-list-item(v-for='req in requests'
|
|
||||||
:key='req.requestId'
|
|
||||||
:request='req')
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { createComponent, onMounted } from '@vue/composition-api'
|
|
||||||
|
|
||||||
import RequestListItem from './RequestListItem.vue'
|
|
||||||
|
|
||||||
import { JournalRequest } from '../../store/types' // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
components: { RequestListItem },
|
|
||||||
props: {
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
requests: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup (props, { parent }) {
|
|
||||||
// TODO: custom events; does this work?
|
|
||||||
onMounted(function () {
|
|
||||||
this.$on('requestUnsnoozed', parent?.$emit('requestUnsnoozed'))
|
|
||||||
this.$on('requestNowShown', parent?.$emit('requestNowShown'))
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
title: props.title,
|
|
||||||
requests: props.requests as JournalRequest[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,118 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
md-table-row
|
|
||||||
md-table-cell.mpj-action-cell.mpj-valign-top
|
|
||||||
md-button(@click='viewFull').md-icon-button.md-raised
|
|
||||||
md-icon description
|
|
||||||
md-tooltip(md-direction='top'
|
|
||||||
md-delay=250) View Full Request
|
|
||||||
template(v-if='!isAnswered.value')
|
|
||||||
md-button(@click='editRequest').md-icon-button.md-raised
|
|
||||||
md-icon edit
|
|
||||||
md-tooltip(md-direction='top'
|
|
||||||
md-delay=250) Edit Request
|
|
||||||
template(v-if='isSnoozed.value')
|
|
||||||
md-button(@click='cancelSnooze()').md-icon-button.md-raised
|
|
||||||
md-icon restore
|
|
||||||
md-tooltip(md-direction='top'
|
|
||||||
md-delay=250) Cancel Snooze
|
|
||||||
template(v-if='isPending.value')
|
|
||||||
md-button(@click='showNow()').md-icon-button.md-raised
|
|
||||||
md-icon restore
|
|
||||||
md-tooltip(md-direction='top'
|
|
||||||
md-delay=250) Show Now
|
|
||||||
md-table-cell.mpj-valign-top
|
|
||||||
p.mpj-request-text {{ request.text }}
|
|
||||||
br(v-if='isSnoozed.value || isPending.value || isAnswered.value')
|
|
||||||
small(v-if='isSnoozed.value').mpj-muted-text: em Snooze expires #[date-from-now(:value='request.snoozedUntil')]
|
|
||||||
small(v-if='isPending.value').mpj-muted-text: em Request appears next #[date-from-now(:value='request.showAfter')]
|
|
||||||
small(v-if='isAnswered.value').mpj-muted-text: em Answered #[date-from-now(:value='request.asOf')]
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, createComponent } from '@vue/composition-api'
|
|
||||||
|
|
||||||
import { Actions, JournalRequest, SnoozeRequestAction, ShowRequestAction } from '../../store/types' // eslint-disable-line no-unused-vars
|
|
||||||
import { useStore } from '../../plugins/store'
|
|
||||||
import { useRouter } from '../../plugins/router'
|
|
||||||
import { useProgress, useSnackbar } from '../../App.vue'
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
props: {
|
|
||||||
request: {
|
|
||||||
type: JournalRequest,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup (props, { parent }) {
|
|
||||||
/** Shorthand for props.request */
|
|
||||||
const r = props.request
|
|
||||||
|
|
||||||
/** The Vuex store */
|
|
||||||
const store = useStore()
|
|
||||||
|
|
||||||
/** The application router */
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
/** The snackbar instance */
|
|
||||||
const snackbar = useSnackbar()
|
|
||||||
|
|
||||||
/** The progress bar component instance */
|
|
||||||
const progress = useProgress()
|
|
||||||
|
|
||||||
/** Whether the request has been answered */
|
|
||||||
const isAnswered = computed(() => r.lastStatus === 'Answered')
|
|
||||||
|
|
||||||
/** Whether the request is snoozed */
|
|
||||||
const isSnoozed = computed(() => r.snoozedUntil > Date.now())
|
|
||||||
|
|
||||||
/** Whether the request is not shown because of an interval */
|
|
||||||
const isPending = computed(() => !isSnoozed.value && r.showAfter > Date.now())
|
|
||||||
|
|
||||||
/** Cancel the snooze period for this request */
|
|
||||||
const cancelSnooze = async () => {
|
|
||||||
const opts: SnoozeRequestAction = {
|
|
||||||
progress,
|
|
||||||
requestId: r.requestId,
|
|
||||||
until: 0
|
|
||||||
}
|
|
||||||
await store.dispatch(Actions.SnoozeRequest, opts)
|
|
||||||
snackbar.events.$emit('info', 'Request un-snoozed')
|
|
||||||
if (parent) parent.$emit('requestUnsnoozed')
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Edit the given request */
|
|
||||||
const editRequest = () => { router.push({ name: 'EditRequest', params: { id: r.requestId } }) }
|
|
||||||
|
|
||||||
/** Show the request now */
|
|
||||||
const showNow = async () => {
|
|
||||||
const opts: ShowRequestAction = {
|
|
||||||
progress,
|
|
||||||
requestId: r.requestId,
|
|
||||||
showAfter: 0
|
|
||||||
}
|
|
||||||
await store.dispatch(Actions.ShowRequestNow, opts)
|
|
||||||
snackbar.events.$emit('info', 'Recurrence skipped; request now shows in journal')
|
|
||||||
if (parent) parent.$emit('requestNowShown')
|
|
||||||
}
|
|
||||||
|
|
||||||
/** View the full request */
|
|
||||||
const viewFull = () => { router.push({ name: 'FullRequest', params: { id: r.requestId } }) }
|
|
||||||
|
|
||||||
return {
|
|
||||||
cancelSnooze,
|
|
||||||
editRequest,
|
|
||||||
isAnswered,
|
|
||||||
isPending,
|
|
||||||
isSnoozed,
|
|
||||||
showNow,
|
|
||||||
viewFull
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="sass">
|
|
||||||
.mpj-action-cell
|
|
||||||
width: 1%
|
|
||||||
white-space: nowrap
|
|
||||||
</style>
|
|
@ -1,96 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
md-dialog(:md-active.sync='isVisible.value').mpj-skinny
|
|
||||||
md-dialog-title Snooze Prayer Request
|
|
||||||
md-content.mpj-dialog-content
|
|
||||||
span.mpj-text-muted Until
|
|
||||||
md-datepicker(v-model='form.snoozedUntil'
|
|
||||||
:md-disabled-dates='datesInPast'
|
|
||||||
md-immediately)
|
|
||||||
md-dialog-actions
|
|
||||||
md-button(:disabled='!isValid.value'
|
|
||||||
@click='snoozeRequest()').md-primary #[md-icon snooze] Snooze
|
|
||||||
md-button(@click='closeDialog()') #[md-icon undo] Cancel
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { createComponent, ref, computed } from '@vue/composition-api'
|
|
||||||
|
|
||||||
import { Actions, SnoozeRequestAction } from '../../store/types' // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
import { useProgress, useSnackbar } from '../../App.vue'
|
|
||||||
import { useEvents } from '../Journal.vue'
|
|
||||||
import { useStore } from '../../plugins/store'
|
|
||||||
|
|
||||||
/** The input form */
|
|
||||||
class SnoozeForm {
|
|
||||||
/** The ID of the request */
|
|
||||||
requestId = ''
|
|
||||||
|
|
||||||
/** The date until which the request will be snoozed */
|
|
||||||
snoozedUntil = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
setup () {
|
|
||||||
/** The Vuex store */
|
|
||||||
const store = useStore()
|
|
||||||
|
|
||||||
/** The progress bar component properties */
|
|
||||||
const progress = useProgress()
|
|
||||||
|
|
||||||
/** The snackbar component properties */
|
|
||||||
const snackbar = useSnackbar()
|
|
||||||
|
|
||||||
/** The journal event bus */
|
|
||||||
const events = useEvents()
|
|
||||||
|
|
||||||
/** Whether this dialog is visible */
|
|
||||||
const isVisible = ref(false)
|
|
||||||
|
|
||||||
/** The input form */
|
|
||||||
const form = new SnoozeForm()
|
|
||||||
|
|
||||||
/** Is the input date valid? */
|
|
||||||
const isValid = computed(() => !isNaN(Date.parse(form.snoozedUntil)))
|
|
||||||
|
|
||||||
/** Close the dialog */
|
|
||||||
const closeDialog = () => {
|
|
||||||
form.requestId = ''
|
|
||||||
form.snoozedUntil = ''
|
|
||||||
isVisible.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open the dialog
|
|
||||||
* @param requestId The ID of the request to be snoozed
|
|
||||||
*/
|
|
||||||
const openDialog = (requestId: string) => {
|
|
||||||
form.requestId = requestId
|
|
||||||
isVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const snoozeRequest = async () => {
|
|
||||||
const opts: SnoozeRequestAction = {
|
|
||||||
progress,
|
|
||||||
requestId: form.requestId,
|
|
||||||
until: Date.parse(form.snoozedUntil)
|
|
||||||
}
|
|
||||||
await store.dispatch(Actions.SnoozeRequest, opts)
|
|
||||||
snackbar.events.$emit('info', `Request snoozed until ${form.snoozedUntil}`)
|
|
||||||
closeDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
events.$on('snooze', openDialog)
|
|
||||||
|
|
||||||
return {
|
|
||||||
closeDialog,
|
|
||||||
datesInPast: (date: Date) => date < new Date(),
|
|
||||||
form,
|
|
||||||
isValid,
|
|
||||||
isVisible,
|
|
||||||
openDialog,
|
|
||||||
snoozeRequest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,64 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
article.mpj-main-content(role='main')
|
|
||||||
page-title(title='Snoozed Requests'
|
|
||||||
hide-on-page=true)
|
|
||||||
template(v-if='isLoaded.value')
|
|
||||||
md-empty-state(v-if='requests.length === 0'
|
|
||||||
md-icon='sentiment_dissatisfied'
|
|
||||||
md-label='No Snoozed Requests'
|
|
||||||
md-description='Your prayer journal has no snoozed requests')
|
|
||||||
md-button(to='/journal').md-primary.md-raised Return to your journal
|
|
||||||
request-list(v-if='requests.length !== 0'
|
|
||||||
title='Snoozed Requests'
|
|
||||||
:requests='requests')
|
|
||||||
p(v-else) Loading journal...
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { createComponent, ref, onMounted } from '@vue/composition-api'
|
|
||||||
import { Store } from 'vuex' // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
import RequestList from './RequestList.vue'
|
|
||||||
|
|
||||||
import { Actions, AppState, JournalRequest } from '../../store/types' // eslint-disable-line no-unused-vars
|
|
||||||
import { useStore } from '../../plugins/store'
|
|
||||||
import { useProgress } from '../../App.vue'
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
components: { RequestList },
|
|
||||||
setup () {
|
|
||||||
/** The Vuex store */
|
|
||||||
const store = useStore() as Store<AppState>
|
|
||||||
|
|
||||||
/** The progress bar component properties */
|
|
||||||
const progress = useProgress()
|
|
||||||
|
|
||||||
/** The snoozed requests */
|
|
||||||
let requests: JournalRequest[] = []
|
|
||||||
|
|
||||||
/** Have snoozed requests been loaded? */
|
|
||||||
const isLoaded = ref(false)
|
|
||||||
|
|
||||||
/** Ensure the latest journal is loaded, and filter it to snoozed requests */
|
|
||||||
const ensureJournal = async () => {
|
|
||||||
if (!Array.isArray(store.state.journal)) {
|
|
||||||
isLoaded.value = false
|
|
||||||
await store.dispatch(Actions.LoadJournal, progress)
|
|
||||||
}
|
|
||||||
requests = store.state.journal
|
|
||||||
.filter(req => req.snoozedUntil > Date.now())
|
|
||||||
.sort((a, b) => a.snoozedUntil - b.snoozedUntil)
|
|
||||||
isLoaded.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(ensureJournal)
|
|
||||||
|
|
||||||
// this.$on('requestUnsnoozed', ensureJournal)
|
|
||||||
|
|
||||||
return {
|
|
||||||
requests,
|
|
||||||
isLoaded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,41 +0,0 @@
|
|||||||
<template lang="pug">
|
|
||||||
article.mpj-main-content(role='main')
|
|
||||||
pageTitle(title='Logging On')
|
|
||||||
p Logging you on...
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { createComponent, onBeforeMount } from '@vue/composition-api'
|
|
||||||
import VueRouter from 'vue-router' // eslint-disable-line no-unused-vars
|
|
||||||
import { Store } from 'vuex' // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
import { AppState } from '../../store/types' // eslint-disable-line no-unused-vars
|
|
||||||
import { AuthService } from '../../auth' // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
import { useAuth } from '../../plugins/auth'
|
|
||||||
import { useRouter } from '../../plugins/router'
|
|
||||||
import { useStore } from '../../plugins/store'
|
|
||||||
|
|
||||||
export default createComponent({
|
|
||||||
setup () {
|
|
||||||
/** Auth service instance */
|
|
||||||
const auth = useAuth() as AuthService
|
|
||||||
|
|
||||||
/** Store instance */
|
|
||||||
const store = useStore() as Store<AppState>
|
|
||||||
|
|
||||||
/** Router instance */
|
|
||||||
const router = useRouter() as VueRouter
|
|
||||||
|
|
||||||
/** Navigate on auth completion */
|
|
||||||
auth.on('loginEvent', (data: any) => {
|
|
||||||
router.push(data.state.target || '/journal')
|
|
||||||
})
|
|
||||||
|
|
||||||
// this.progress.$emit('show', 'indeterminate')
|
|
||||||
onBeforeMount(async () => { await auth.handleAuthentication(store) })
|
|
||||||
|
|
||||||
return { }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -1,64 +0,0 @@
|
|||||||
// Vue packages and components
|
|
||||||
import Vue from 'vue'
|
|
||||||
import VueCompositionApi from '@vue/composition-api'
|
|
||||||
import {
|
|
||||||
MdApp,
|
|
||||||
MdButton,
|
|
||||||
MdCard,
|
|
||||||
MdContent,
|
|
||||||
MdDatepicker,
|
|
||||||
MdDialog,
|
|
||||||
MdEmptyState,
|
|
||||||
MdField,
|
|
||||||
MdIcon,
|
|
||||||
MdLayout,
|
|
||||||
MdProgress,
|
|
||||||
MdRadio,
|
|
||||||
MdSnackbar,
|
|
||||||
MdTable,
|
|
||||||
MdTabs,
|
|
||||||
MdToolbar,
|
|
||||||
MdTooltip
|
|
||||||
} from 'vue-material/dist/components'
|
|
||||||
|
|
||||||
// myPrayerJournal components
|
|
||||||
import App from './App.vue'
|
|
||||||
import router from './router'
|
|
||||||
import store from './store'
|
|
||||||
import DateFromNow from './components/common/DateFromNow.vue'
|
|
||||||
import PageTitle from './components/common/PageTitle.vue'
|
|
||||||
import AuthPlugin from './plugins/auth'
|
|
||||||
|
|
||||||
// Styles
|
|
||||||
import 'vue-material/dist/vue-material.min.css'
|
|
||||||
import 'vue-material/dist/theme/default.css'
|
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
|
||||||
|
|
||||||
Vue.use(MdApp)
|
|
||||||
Vue.use(MdButton)
|
|
||||||
Vue.use(MdCard)
|
|
||||||
Vue.use(MdContent)
|
|
||||||
Vue.use(MdDatepicker)
|
|
||||||
Vue.use(MdDialog)
|
|
||||||
Vue.use(MdEmptyState)
|
|
||||||
Vue.use(MdField)
|
|
||||||
Vue.use(MdIcon)
|
|
||||||
Vue.use(MdLayout)
|
|
||||||
Vue.use(MdProgress)
|
|
||||||
Vue.use(MdRadio)
|
|
||||||
Vue.use(MdSnackbar)
|
|
||||||
Vue.use(MdTable)
|
|
||||||
Vue.use(MdTabs)
|
|
||||||
Vue.use(MdToolbar)
|
|
||||||
Vue.use(MdTooltip)
|
|
||||||
Vue.use(AuthPlugin)
|
|
||||||
Vue.use(VueCompositionApi)
|
|
||||||
Vue.component('date-from-now', DateFromNow)
|
|
||||||
Vue.component('page-title', PageTitle)
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
router,
|
|
||||||
store,
|
|
||||||
render: h => h(App)
|
|
||||||
}).$mount('#app')
|
|
@ -1,36 +0,0 @@
|
|||||||
import { inject, provide } from '@vue/composition-api'
|
|
||||||
import authService, { AuthService } from '@/auth'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
install (Vue: any) {
|
|
||||||
Vue.prototype.$auth = authService
|
|
||||||
|
|
||||||
Vue.mixin({
|
|
||||||
created () {
|
|
||||||
if (this.handleLoginEvent) {
|
|
||||||
authService.addListener('loginEvent', this.handleLoginEvent)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
destroyed () {
|
|
||||||
if (this.handleLoginEvent) {
|
|
||||||
authService.removeListener('loginEvent', this.handleLoginEvent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const AuthSymbol = Symbol('Auth service')
|
|
||||||
|
|
||||||
export function provideAuth (auth: AuthService) {
|
|
||||||
provide(AuthSymbol, auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Use the auth service */
|
|
||||||
export function useAuth (): AuthService {
|
|
||||||
const auth = inject(AuthSymbol)
|
|
||||||
if (!auth) {
|
|
||||||
throw new Error('Auth not configured!')
|
|
||||||
}
|
|
||||||
return auth as AuthService
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
import VueRouter from 'vue-router'
|
|
||||||
import { inject, provide } from '@vue/composition-api'
|
|
||||||
|
|
||||||
const RouterSymbol = Symbol('Vue router')
|
|
||||||
|
|
||||||
export function provideRouter (router: VueRouter) {
|
|
||||||
provide(RouterSymbol, router)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRouter (): VueRouter {
|
|
||||||
const router = inject(RouterSymbol)
|
|
||||||
if (!router) {
|
|
||||||
throw new Error('Router not configured!')
|
|
||||||
}
|
|
||||||
return router as VueRouter
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { provide, inject } from '@vue/composition-api'
|
|
||||||
import { Store } from 'vuex'
|
|
||||||
|
|
||||||
import { AppState } from '@/store/types'
|
|
||||||
|
|
||||||
const StoreSymbol = Symbol('Vuex store')
|
|
||||||
|
|
||||||
/** Configure the store provided by this plugin */
|
|
||||||
export function provideStore (store: Store<AppState>) {
|
|
||||||
provide(StoreSymbol, store)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Use the provided store */
|
|
||||||
export function useStore (): Store<AppState> {
|
|
||||||
const store = inject(StoreSymbol)
|
|
||||||
if (!store) {
|
|
||||||
throw new Error('No store configured!')
|
|
||||||
}
|
|
||||||
return store as Store<AppState>
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import Router from 'vue-router'
|
|
||||||
|
|
||||||
import auth from '@/auth'
|
|
||||||
import Home from '@/components/Home.vue'
|
|
||||||
|
|
||||||
Vue.use(Router)
|
|
||||||
|
|
||||||
const router = new Router({
|
|
||||||
mode: 'history',
|
|
||||||
base: process.env.BASE_URL,
|
|
||||||
scrollBehavior (to, from, savedPosition) {
|
|
||||||
if (savedPosition) {
|
|
||||||
return savedPosition
|
|
||||||
} else {
|
|
||||||
return { x: 0, y: 0 }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
name: 'Home',
|
|
||||||
component: Home
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/journal',
|
|
||||||
name: 'Journal',
|
|
||||||
component: () => import('@/components/Journal.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/legal/privacy-policy',
|
|
||||||
name: 'PrivacyPolicy',
|
|
||||||
component: () => import('@/components/legal/PrivacyPolicy.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/legal/terms-of-service',
|
|
||||||
name: 'TermsOfService',
|
|
||||||
component: () => import('@/components/legal/TermsOfService.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/request/:id/edit',
|
|
||||||
name: 'EditRequest',
|
|
||||||
component: () => import('@/components/request/EditRequest.vue'),
|
|
||||||
props: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/request/:id/full',
|
|
||||||
name: 'FullRequest',
|
|
||||||
component: () => import('@/components/request/FullRequest.vue'),
|
|
||||||
props: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/requests/active',
|
|
||||||
name: 'ActiveRequests',
|
|
||||||
component: () => import('@/components/request/ActiveRequests.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/requests/answered',
|
|
||||||
name: 'AnsweredRequests',
|
|
||||||
component: () => import('@/components/request/AnsweredRequests.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/requests/snoozed',
|
|
||||||
name: 'SnoozedRequests',
|
|
||||||
component: () => import('@/components/request/SnoozedRequests.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/user/log-on',
|
|
||||||
name: 'LogOn',
|
|
||||||
component: () => import('@/components/user/LogOn.vue')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
|
||||||
if (to.path === '/' || to.path === '/user/log-on' || auth.isAuthenticated()) {
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
auth.login({ target: to.path })
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
|
13
src/app/src/shims-tsx.d.ts
vendored
13
src/app/src/shims-tsx.d.ts
vendored
@ -1,13 +0,0 @@
|
|||||||
import Vue, { VNode } from 'vue'
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
namespace JSX {
|
|
||||||
// tslint:disable no-empty-interface
|
|
||||||
interface Element extends VNode {}
|
|
||||||
// tslint:disable no-empty-interface
|
|
||||||
interface ElementClass extends Vue {}
|
|
||||||
interface IntrinsicElements {
|
|
||||||
[elem: string]: any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
4
src/app/src/shims-vue.d.ts
vendored
4
src/app/src/shims-vue.d.ts
vendored
@ -1,4 +0,0 @@
|
|||||||
declare module '*.vue' {
|
|
||||||
import Vue from 'vue'
|
|
||||||
export default Vue
|
|
||||||
}
|
|
@ -1,197 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import Vuex, { StoreOptions } from 'vuex'
|
|
||||||
|
|
||||||
import api from '@/api'
|
|
||||||
import auth from '@/auth'
|
|
||||||
|
|
||||||
import {
|
|
||||||
AppState,
|
|
||||||
Actions,
|
|
||||||
JournalRequest,
|
|
||||||
Mutations,
|
|
||||||
SnoozeRequestAction,
|
|
||||||
ShowRequestAction,
|
|
||||||
AddRequestAction,
|
|
||||||
UpdateRequestAction
|
|
||||||
} from './types'
|
|
||||||
import { ProgressProps } from '@/types'
|
|
||||||
|
|
||||||
Vue.use(Vuex)
|
|
||||||
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
const logError = function (error: any) { // TODO: can we do better on this type?
|
|
||||||
if (error.response) {
|
|
||||||
// The request was made and the server responded with a status code
|
|
||||||
// that falls out of the range of 2xx
|
|
||||||
console.error(error.response.data)
|
|
||||||
console.error(error.response.status)
|
|
||||||
console.error(error.response.headers)
|
|
||||||
} else if (error.request) {
|
|
||||||
// The request was made but no response was received
|
|
||||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
|
||||||
// http.ClientRequest in node.js
|
|
||||||
console.error(error.request)
|
|
||||||
} else {
|
|
||||||
// Something happened in setting up the request that triggered an Error
|
|
||||||
console.error('Error', error.message)
|
|
||||||
}
|
|
||||||
console.error(`config: ${error.config}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the "Bearer" authorization header with the current access token
|
|
||||||
*/
|
|
||||||
const setBearer = async function () {
|
|
||||||
try {
|
|
||||||
await auth.getAccessToken()
|
|
||||||
api.setBearer(auth.session.id.token)
|
|
||||||
} catch (err) {
|
|
||||||
if (err.message === 'Not logged in') {
|
|
||||||
console.warn('API request attempted when user was not logged in')
|
|
||||||
} else {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* eslint-enable no-console */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the sort value for a prayer request
|
|
||||||
* @param x The prayer request
|
|
||||||
*/
|
|
||||||
const sortValue = (x: JournalRequest) => x.showAfter === 0 ? x.asOf : x.showAfter
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sort journal requests either by asOf or showAfter
|
|
||||||
*/
|
|
||||||
const journalSort = (a: JournalRequest, b: JournalRequest) => sortValue(a) - sortValue(b)
|
|
||||||
|
|
||||||
/** The initial state of the store */
|
|
||||||
const store : StoreOptions<AppState> = {
|
|
||||||
state: {
|
|
||||||
user: auth.session.profile,
|
|
||||||
isAuthenticated: auth.isAuthenticated(),
|
|
||||||
journal: [],
|
|
||||||
isLoadingJournal: false
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
[Mutations.LoadingJournal] (state, flag: boolean) {
|
|
||||||
state.isLoadingJournal = flag
|
|
||||||
},
|
|
||||||
[Mutations.LoadedJournal] (state, journal: JournalRequest[]) {
|
|
||||||
state.journal = journal.sort(journalSort)
|
|
||||||
},
|
|
||||||
[Mutations.RequestAdded] (state, newRequest: JournalRequest) {
|
|
||||||
state.journal.push(newRequest)
|
|
||||||
},
|
|
||||||
[Mutations.RequestUpdated] (state, request: JournalRequest) {
|
|
||||||
const jrnl = state.journal.filter(it => it.requestId !== request.requestId)
|
|
||||||
if (request.lastStatus !== 'Answered') jrnl.push(request)
|
|
||||||
state.journal = jrnl
|
|
||||||
},
|
|
||||||
[Mutations.SetAuthentication] (state, value: boolean) {
|
|
||||||
state.isAuthenticated = value
|
|
||||||
},
|
|
||||||
[Mutations.UserLoggedOff] (state) {
|
|
||||||
state.user = {}
|
|
||||||
api.removeBearer()
|
|
||||||
state.isAuthenticated = false
|
|
||||||
},
|
|
||||||
[Mutations.UserLoggedOn] (state, user) {
|
|
||||||
state.user = user
|
|
||||||
state.isAuthenticated = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
async [Actions.AddRequest] ({ commit }, p: AddRequestAction) {
|
|
||||||
const { progress, requestText, recurType, recurCount } = p
|
|
||||||
progress.events.$emit('show', 'indeterminate')
|
|
||||||
try {
|
|
||||||
await setBearer()
|
|
||||||
const newRequest = await api.addRequest(requestText, recurType, recurCount)
|
|
||||||
commit(Mutations.RequestAdded, newRequest.data)
|
|
||||||
} catch (err) {
|
|
||||||
logError(err)
|
|
||||||
} finally {
|
|
||||||
progress.events.$emit('done')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async [Actions.CheckAuthentication] ({ commit }) {
|
|
||||||
try {
|
|
||||||
await auth.getAccessToken()
|
|
||||||
commit(Mutations.SetAuthentication, auth.isAuthenticated())
|
|
||||||
} catch (_) {
|
|
||||||
commit(Mutations.SetAuthentication, false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async [Actions.LoadJournal] ({ commit }, progress: ProgressProps) {
|
|
||||||
commit(Mutations.LoadedJournal, [])
|
|
||||||
progress.events.$emit('show', 'query')
|
|
||||||
commit(Mutations.LoadingJournal, true)
|
|
||||||
await setBearer()
|
|
||||||
try {
|
|
||||||
const jrnl = await api.journal()
|
|
||||||
commit(Mutations.LoadedJournal, jrnl.data)
|
|
||||||
} catch (err) {
|
|
||||||
logError(err)
|
|
||||||
} finally {
|
|
||||||
progress.events.$emit('done')
|
|
||||||
commit(Mutations.LoadingJournal, false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async [Actions.UpdateRequest] ({ commit, state }, p: UpdateRequestAction) {
|
|
||||||
const { progress, requestId, status, updateText, recurType, recurCount } = p
|
|
||||||
progress.events.$emit('show', 'indeterminate')
|
|
||||||
try {
|
|
||||||
await setBearer()
|
|
||||||
const oldReq = (state.journal.filter(req => req.requestId === requestId) || [])[0] || {}
|
|
||||||
if (!(status === 'Prayed' && updateText === '')) {
|
|
||||||
if (status !== 'Answered' && (oldReq.recurType !== recurType || oldReq.recurCount !== recurCount)) {
|
|
||||||
await api.updateRecurrence(requestId, recurType, recurCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (status !== 'Updated' || oldReq.text !== updateText) {
|
|
||||||
await api.updateRequest(requestId, status, oldReq.text !== updateText ? updateText : '')
|
|
||||||
}
|
|
||||||
const request = await api.getRequest(requestId)
|
|
||||||
commit(Mutations.RequestUpdated, request.data)
|
|
||||||
} catch (err) {
|
|
||||||
logError(err)
|
|
||||||
} finally {
|
|
||||||
progress.events.$emit('done')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async [Actions.ShowRequestNow] ({ commit }, p: ShowRequestAction) {
|
|
||||||
const { progress, requestId, showAfter } = p
|
|
||||||
progress.events.$emit('show', 'indeterminate')
|
|
||||||
try {
|
|
||||||
await setBearer()
|
|
||||||
await api.showRequest(requestId, showAfter)
|
|
||||||
const request = await api.getRequest(requestId)
|
|
||||||
commit(Mutations.RequestUpdated, request.data)
|
|
||||||
} catch (err) {
|
|
||||||
logError(err)
|
|
||||||
} finally {
|
|
||||||
progress.events.$emit('done')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async [Actions.SnoozeRequest] ({ commit }, p: SnoozeRequestAction) {
|
|
||||||
const { progress, requestId, until } = p
|
|
||||||
progress.events.$emit('show', 'indeterminate')
|
|
||||||
try {
|
|
||||||
await setBearer()
|
|
||||||
await api.snoozeRequest(requestId, until)
|
|
||||||
const request = await api.getRequest(requestId)
|
|
||||||
commit(Mutations.RequestUpdated, request.data)
|
|
||||||
} catch (err) {
|
|
||||||
logError(err)
|
|
||||||
} finally {
|
|
||||||
progress.events.$emit('done')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getters: {},
|
|
||||||
modules: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new Vuex.Store<AppState>(store)
|
|
@ -1,166 +0,0 @@
|
|||||||
import { ProgressProps } from '@/types'
|
|
||||||
|
|
||||||
/** A history entry for a prayer request */
|
|
||||||
export class HistoryEntry {
|
|
||||||
/** The status for this history entry */
|
|
||||||
status = '' // TODO string union?
|
|
||||||
|
|
||||||
/** The date/time for this history entry */
|
|
||||||
asOf = 0
|
|
||||||
|
|
||||||
/** The text of this history entry */
|
|
||||||
text?: string = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An entry with notes for a request */
|
|
||||||
export class NotesEntry {
|
|
||||||
/** The date/time the notes were recorded */
|
|
||||||
asOf = 0
|
|
||||||
|
|
||||||
/** The notes */
|
|
||||||
notes = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A prayer request that is part of the user's journal */
|
|
||||||
export class JournalRequest {
|
|
||||||
/** The ID of the request (just the CUID part) */
|
|
||||||
requestId = ''
|
|
||||||
|
|
||||||
/** The ID of the user to whom the request belongs */
|
|
||||||
userId = ''
|
|
||||||
|
|
||||||
/** The current text of the request */
|
|
||||||
text = ''
|
|
||||||
|
|
||||||
/** The last time action was taken on the request */
|
|
||||||
asOf = 0
|
|
||||||
|
|
||||||
/** The last status for the request */
|
|
||||||
lastStatus = '' // TODO string union?
|
|
||||||
|
|
||||||
/** The time that this request should reappear in the user's journal */
|
|
||||||
snoozedUntil = 0
|
|
||||||
|
|
||||||
/** The time after which this request should reappear in the user's journal by configured recurrence */
|
|
||||||
showAfter = 0
|
|
||||||
|
|
||||||
/** The type of recurrence for this request */
|
|
||||||
recurType = '' // TODO Recurrence union?
|
|
||||||
|
|
||||||
/** How many of the recurrence intervals should occur between appearances in the journal */
|
|
||||||
recurCount = 0
|
|
||||||
|
|
||||||
/** History entries for the request */
|
|
||||||
history: HistoryEntry[] = []
|
|
||||||
|
|
||||||
/** Note entries for the request */
|
|
||||||
notes: NotesEntry[] = []
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The state of myPrayerJournal */
|
|
||||||
export interface AppState {
|
|
||||||
/** The user's profile */
|
|
||||||
user: any,
|
|
||||||
|
|
||||||
/** Whether there is a user signed in */
|
|
||||||
isAuthenticated: boolean,
|
|
||||||
|
|
||||||
/** The current set of prayer requests */
|
|
||||||
journal: JournalRequest[],
|
|
||||||
|
|
||||||
/** Whether the journal is currently being loaded */
|
|
||||||
isLoadingJournal: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
/** Action to add a prayer request (pass request text) */
|
|
||||||
AddRequest: 'add-request',
|
|
||||||
|
|
||||||
/** Action to check if a user is authenticated, refreshing the session first if it exists */
|
|
||||||
CheckAuthentication: 'check-authentication',
|
|
||||||
|
|
||||||
/** Action to load the user's prayer journal */
|
|
||||||
LoadJournal: 'load-journal',
|
|
||||||
|
|
||||||
/** Action to update a request */
|
|
||||||
UpdateRequest: 'update-request',
|
|
||||||
|
|
||||||
/** Action to skip the remaining recurrence period */
|
|
||||||
ShowRequestNow: 'show-request-now',
|
|
||||||
|
|
||||||
/** Action to snooze a request */
|
|
||||||
SnoozeRequest: 'snooze-request'
|
|
||||||
}
|
|
||||||
export { actions as Actions }
|
|
||||||
|
|
||||||
const mutations = {
|
|
||||||
/** Mutation for when the user's prayer journal is being loaded */
|
|
||||||
LoadingJournal: 'loading-journal',
|
|
||||||
|
|
||||||
/** Mutation for when the user's prayer journal has been loaded */
|
|
||||||
LoadedJournal: 'journal-loaded',
|
|
||||||
|
|
||||||
/** Mutation for adding a new prayer request (pass text) */
|
|
||||||
RequestAdded: 'request-added',
|
|
||||||
|
|
||||||
/** Mutation to replace a prayer request at the top of the current journal */
|
|
||||||
RequestUpdated: 'request-updated',
|
|
||||||
|
|
||||||
/** Mutation for setting the authentication state */
|
|
||||||
SetAuthentication: 'set-authentication',
|
|
||||||
|
|
||||||
/** Mutation for logging a user off */
|
|
||||||
UserLoggedOff: 'user-logged-off',
|
|
||||||
|
|
||||||
/** Mutation for logging a user on (pass user) */
|
|
||||||
UserLoggedOn: 'user-logged-on'
|
|
||||||
}
|
|
||||||
export { mutations as Mutations }
|
|
||||||
|
|
||||||
/** The shape of the parameter to the add request action */
|
|
||||||
export interface AddRequestAction {
|
|
||||||
/** The progress bar component properties */
|
|
||||||
progress: ProgressProps
|
|
||||||
/** The text of the request */
|
|
||||||
requestText: string
|
|
||||||
/** The recurrence type */
|
|
||||||
recurType: string
|
|
||||||
/** The number of intervals for non-immediate recurrence */
|
|
||||||
recurCount: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The shape of the parameter to the show request action */
|
|
||||||
export interface ShowRequestAction {
|
|
||||||
/** The progress bar component properties */
|
|
||||||
progress: ProgressProps
|
|
||||||
/** The ID of the prayer request being shown */
|
|
||||||
requestId: string
|
|
||||||
/** The date/time after which the request will be once again shown */
|
|
||||||
showAfter: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The shape of the parameter to the snooze request action */
|
|
||||||
export interface SnoozeRequestAction {
|
|
||||||
/** The progress bar component properties */
|
|
||||||
progress: ProgressProps
|
|
||||||
/** The ID of the prayer request being snoozed/unsnoozed */
|
|
||||||
requestId: string
|
|
||||||
/** The date/time after which the request will be once again shown */
|
|
||||||
until: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The shape of the parameter to the update request action */
|
|
||||||
export interface UpdateRequestAction {
|
|
||||||
/** The progress bar component properties */
|
|
||||||
progress: ProgressProps
|
|
||||||
/** The ID of the prayer request */
|
|
||||||
requestId: string
|
|
||||||
/** The text of the update */
|
|
||||||
updateText: string
|
|
||||||
/** The status associated with the update */
|
|
||||||
status: string
|
|
||||||
/** The type of recurrence for the request */
|
|
||||||
recurType: string
|
|
||||||
/** The number of intervals for non-immediate recurrence */
|
|
||||||
recurCount: number
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import { Ref } from '@vue/composition-api';
|
|
||||||
|
|
||||||
export interface SnackbarProps {
|
|
||||||
events: Vue
|
|
||||||
visible: Ref<boolean>
|
|
||||||
message: Ref<string>
|
|
||||||
interval: Ref<number>
|
|
||||||
showSnackbar: (msg: string) => void
|
|
||||||
showInfo: (msg: string) => void
|
|
||||||
showError: (msg: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProgressProps {
|
|
||||||
events: Vue
|
|
||||||
visible: Ref<boolean>
|
|
||||||
mode: Ref<string>
|
|
||||||
showProgress: (mod: string) => void
|
|
||||||
hideProgress: () => void
|
|
||||||
}
|
|
1
src/app/src/untyped-modules.d.ts
vendored
1
src/app/src/untyped-modules.d.ts
vendored
@ -1 +0,0 @@
|
|||||||
declare module 'vue-material/dist/components'
|
|
@ -1,22 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<script src="https://cdn.auth0.com/js/auth0/8.9/auth0.min.js"></script>
|
|
||||||
<script>
|
|
||||||
var webAuth = new auth0.WebAuth({
|
|
||||||
domain: 'djs-consulting.auth0.com',
|
|
||||||
clientID: 'Of2s0RQCQ3mt3dwIkOBY5h85J9sXbF2n',
|
|
||||||
scope: 'openid profile email',
|
|
||||||
responseType: 'token id_token',
|
|
||||||
redirectUri: location.protocol + '//' + location.host + '/static/silent.html'
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
webAuth.parseHash(window.location.hash, function (err, response) {
|
|
||||||
parent.postMessage(err || response, location.protocol + '//' + location.host);
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body></body>
|
|
||||||
</html>
|
|
@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "esnext",
|
|
||||||
"module": "esnext",
|
|
||||||
"strict": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"importHelpers": true,
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"types": [
|
|
||||||
"webpack-env"
|
|
||||||
],
|
|
||||||
"paths": {
|
|
||||||
"@/*": [
|
|
||||||
"src/*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lib": [
|
|
||||||
"esnext",
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"scripthost"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*.ts",
|
|
||||||
"src/**/*.tsx",
|
|
||||||
"src/**/*.vue",
|
|
||||||
"tests/**/*.ts",
|
|
||||||
"tests/**/*.tsx"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
const webpack = require('webpack')
|
|
||||||
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
|
||||||
module.exports = {
|
|
||||||
outputDir: '../MyPrayerJournal.Api/wwwroot',
|
|
||||||
configureWebpack: {
|
|
||||||
plugins: [
|
|
||||||
// new BundleAnalyzerPlugin(),
|
|
||||||
],
|
|
||||||
optimization: {
|
|
||||||
splitChunks: {
|
|
||||||
chunks: 'all'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user