myPrayerJournal v2 (#27)

App changes:
* Move to Vue Material for UI components
* Convert request cards to true material design cards, separating the "pray" button from the others and improved highlighting of the current request
* Centralize Auth0 integration in one place; modify the Vuex store to rely on it entirely, and add a Vue mixin to make it accessible by any component

API changes:
* Change backing data store to RavenDB
* Evolve domain models (using F# discriminated unions, and JSON converters for storage) to make invalid states unrepresentable
* Incorporate the FunctionalCuid library
* Create a functional pipeline for app configuration instead of chaining `IWebHostBuilder` calls

Bug fixes:
* Set showAfter to 0 for immediately recurring requests (#26)
This commit was merged in pull request #27.
This commit is contained in:
2019-09-02 19:01:26 -05:00
committed by GitHub
parent ce588b6a43
commit fa78e86de6
44 changed files with 2729 additions and 1939 deletions

View File

@@ -1,25 +1,24 @@
{
"name": "my-prayer-journal",
"version": "1.2.2",
"version": "2.0.0",
"description": "myPrayerJournal - Front End",
"author": "Daniel J. Summers <daniel@bitbadger.solutions>",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"serve": "vue-cli-service serve --port 8081",
"build": "vue-cli-service build --modern",
"lint": "vue-cli-service lint",
"apistart": "cd ../api/MyPrayerJournal.Api && dotnet run",
"vue": "vue-cli-service build --modern && cd ../api/MyPrayerJournal.Api && dotnet run",
"publish": "vue-cli-service build --modern && cd ../api/MyPrayerJournal.Api && dotnet publish -c Release"
"apistart": "cd ../MyPrayerJournal.Api && dotnet run",
"vue": "vue-cli-service build --modern && cd ../MyPrayerJournal.Api && dotnet run",
"publish": "vue-cli-service build --modern && cd ../MyPrayerJournal.Api && dotnet publish -c Release"
},
"dependencies": {
"auth0-js": "^9.7.3",
"axios": "^0.19.0",
"moment": "^2.18.1",
"vue": "^2.5.15",
"vue-progressbar": "^0.7.3",
"vue-material": "^1.0.0-beta-11",
"vue-router": "^3.0.0",
"vue-toast": "^3.1.0",
"vuex": "^3.0.1"
},
"devDependencies": {
@@ -27,8 +26,11 @@
"@vue/cli-plugin-eslint": "^3.0.0",
"@vue/cli-service": "^3.0.0",
"@vue/eslint-config-standard": "^4.0.0",
"node-sass": "^4.12.0",
"pug": "^2.0.1",
"pug-plain-loader": "^1.0.0",
"vue-template-compiler": "^2.5.17"
"sass-loader": "^7.3.1",
"vue-template-compiler": "^2.5.17",
"webpack-bundle-analyzer": "^3.4.1"
}
}

View File

@@ -1,26 +1,41 @@
<template lang="pug">
#app(role='application')
navigation
#content
router-view
vue-progress-bar
toast(ref='toast')
footer.mpj-text-right.mpj-muted-text
p
| myPrayerJournal v{{ version }}
br
em: small.
#[router-link(:to="{ name: 'PrivacyPolicy' }") Privacy Policy] &bull;
#[router-link(:to="{ name: 'TermsOfService' }") Terms of Service] &bull;
#[a(href='https://github.com/bit-badger/myprayerjournal' target='_blank') Developed] and hosted by
#[a(href='https://bitbadger.solutions' target='_blank') Bit Badger Solutions]
#app.page-container
md-app(md-waterfall md-mode='fixed-last' role='application')
md-app-toolbar.md-large.md-dense.md-primary
.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'
:md-mode='progress.mode')
router-view
md-snackbar(:md-active.sync='snackbar.visible'
md-position='center'
:md-duration='snackbar.interval'
ref='snackbar') {{ snackbar.message }}
footer
p.mpj-muted-text.mpj-text-right
| myPrayerJournal v{{ version }}
br
em: small.
#[router-link(to='/legal/privacy-policy') Privacy Policy] &bull;
#[router-link(to='/legal/terms-of-service') Terms of Service] &bull;
#[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>
'use strict'
import Navigation from './components/common/Navigation.vue'
import Vue from 'vue'
import Navigation from '@/components/common/Navigation'
import actions from '@/store/action-types'
import { version } from '../package.json'
export default {
@@ -29,216 +44,115 @@ export default {
Navigation
},
data () {
return {}
return {
progress: {
events: new Vue(),
visible: false,
mode: 'query'
},
snackbar: {
events: new Vue(),
visible: false,
message: '',
interval: 4000
}
}
},
mounted () {
this.$refs.toast.setOptions({ position: 'bottom right' })
async mounted () {
this.progress.events.$on('show', this.showProgress)
this.progress.events.$on('done', this.hideProgress)
this.snackbar.events.$on('info', this.showInfo)
this.snackbar.events.$on('error', this.showError)
await this.$store.dispatch(actions.CHECK_AUTHENTICATION)
},
computed: {
toast () {
return this.$refs.toast
},
version () {
return version.endsWith('.0') ? version.substr(0, version.length - 2) : version
return version.endsWith('.0')
? version.endsWith('.0.0')
? version.substr(0, version.length - 4)
: version.substr(0, version.length - 2)
: version
}
},
methods: {
showSnackbar (message) {
this.snackbar.message = message
this.snackbar.visible = true
},
showInfo (message) {
this.snackbar.interval = 4000
this.showSnackbar(message)
},
showError (message) {
this.snackbar.interval = Infinity
this.showSnackbar(message)
},
showProgress (mode) {
this.progress.mode = mode
this.progress.visible = true
},
hideProgress () {
this.progress.visible = false
},
handleLoginEvent (data) {
if (!data.loggedIn) {
this.showInfo('Logged out successfully')
}
}
},
provide () {
return {
messages: this.snackbar.events,
progress: this.progress.events
}
}
}
</script>
<style>
html, body {
background-color: whitesmoke;
<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;
}
body {
padding-top: 50px;
margin: 0;
}
h1, h2, h3, h4, h5 {
font-weight: 500;
margin-top: 0;
}
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 2rem;
}
h3 {
font-size: 1.75rem;
}
h4 {
font-size: 1.5rem;
}
h5 {
font-size: 1.25rem;
}
p {
margin-bottom: 0;
}
input, textarea, select {
border-radius: .25rem;
font-size: 1rem;
}
textarea {
font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace;
}
input, select {
font-family: inherit;
}
button,
a[role="button"] {
border: solid 1px #050;
border-radius: .5rem;
background-color: rgb(235, 235, 235);
padding: .25rem;
font-size: 1rem;
}
a[role="button"]:link,
a[role="button"]:visited {
color: black;
}
button.primary,
a[role="button"].primary {
background-color: white;
border-width: 3px;
}
button:hover,
a[role="button"]:hover {
cursor: pointer;
background-color: #050;
color: white;
text-decoration: none;
}
label {
font-variant: small-caps;
font-size: 1.1rem;
}
label.normal {
font-variant: unset;
font-size: unset;
}
footer {
border-top: solid 1px lightgray;
margin-top: 1rem;
padding: 0 1rem;
}
footer p {
margin: 0;
}
a:link, a:visited {
color: #050;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.mpj-main-content {
max-width: 60rem;
margin: auto;
}
.mpj-main-content-wide {
margin: .5rem;
}
@media screen and (max-width: 21rem) {
.mpj-main-content-wide {
margin: 0;
}
}
.mpj-request-text {
white-space: pre-line;
}
.mpj-request-list p {
border-top: solid 1px lightgray;
}
.mpj-request-list p:first-child {
border-top: none;
}
.mpj-request-log {
width: 100%;
}
.mpj-request-log thead th {
border-top: solid 1px lightgray;
border-bottom: solid 2px lightgray;
text-align: left;
}
.mpj-request-log tbody td {
border-bottom: dotted 1px lightgray;
vertical-align: top;
}
.mpj-bg {
background-image: -webkit-gradient(linear, left top, left bottom, from(#050), to(whitesmoke));
background-image: -webkit-linear-gradient(top, #050, whitesmoke);
background-image: -moz-linear-gradient(top, #050, whitesmoke);
background-image: linear-gradient(to bottom, #050, whitesmoke);
}
.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-narrow {
max-width: 40rem;
margin: auto;
}
.mpj-skinny {
max-width: 20rem;
margin: auto;
}
.mpj-full-width {
width: 100%;
}
.mpj-modal {
position: fixed;
z-index: 8;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, .4);
}
.mpj-modal-content {
background-color: whitesmoke;
border: solid 1px #050;
border-radius: .5rem;
animation-name: animatetop;
animation-duration: 0.4s;
padding: 1rem;
margin-top: 4rem;
}
@keyframes animatetop {
from {
top: -300px;
opacity: 0;
}
to {
top: 0;
opacity: 1;
}
}
.mpj-modal-content header {
margin: -1rem -1rem .5rem;
border-radius: .4rem;
}
.mpj-modal-content header h5 {
color: white;
margin: 0;
padding: 1rem;
}
.mpj-margin {
margin-left: 1rem;
margin-right: 1rem;
}
.material-icons {
vertical-align: middle;
}
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-progress-bar
margin: 24px
</style>

View File

@@ -15,12 +15,12 @@ export default {
* Set the bearer token for all future requests
* @param {string} token The token to use to identify the user to the server
*/
setBearer: token => { http.defaults.headers.common['authorization'] = `Bearer ${token}` },
setBearer: token => { http.defaults.headers.common['Authorization'] = `Bearer ${token}` },
/**
* Remove the bearer token
*/
removeBearer: () => delete http.defaults.headers.common['authorization'],
removeBearer: () => delete http.defaults.headers.common['Authorization'],
/**
* Add a note for a prayer request

View File

@@ -1,31 +1,45 @@
'use strict'
import auth0 from 'auth0-js'
/* eslint-disable */
import auth0 from 'auth0-js'
import EventEmitter from 'events'
import AUTH_CONFIG from './auth0-variables'
import mutations from '@/store/mutation-types'
import mutations from '@/store/mutation-types'
/* es-lint-enable*/
var tokenRenewalTimeout
// Auth0 web authentication instance to use for our calls
const webAuth = new auth0.WebAuth({
domain: AUTH_CONFIG.domain,
clientID: AUTH_CONFIG.clientId,
redirectUri: AUTH_CONFIG.appDomain + AUTH_CONFIG.callbackUrl,
audience: `https://${AUTH_CONFIG.domain}/userinfo`,
responseType: 'token id_token',
scope: 'openid profile email'
})
export default class AuthService {
constructor () {
this.login = this.login.bind(this)
this.setSession = this.setSession.bind(this)
this.logout = this.logout.bind(this)
this.isAuthenticated = this.isAuthenticated.bind(this)
/**
* A class to handle all authentication calls and determinations
*/
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 = {}
constructor() {
super()
this.refreshSession()
}
auth0 = new auth0.WebAuth({
domain: AUTH_CONFIG.domain,
clientID: AUTH_CONFIG.clientId,
redirectUri: AUTH_CONFIG.appDomain + AUTH_CONFIG.callbackUrl,
audience: `https://${AUTH_CONFIG.domain}/userinfo`,
responseType: 'token id_token',
scope: 'openid profile email'
})
login () {
this.auth0.authorize()
/**
* Starts the user log in flow
*/
login (customState) {
webAuth.authorize({
appState: customState
})
}
/**
@@ -33,7 +47,7 @@ export default class AuthService {
*/
parseHash () {
return new Promise((resolve, reject) => {
this.auth0.parseHash((err, authResult) => {
webAuth.parseHash((err, authResult) => {
if (err) {
reject(err)
} else {
@@ -44,95 +58,137 @@ export default class AuthService {
}
/**
* Promisified userInfo function
*
* @param token The auth token from the login result
* Handle authentication replies from Auth0
*
* @param store The Vuex store
*/
userInfo (token) {
return new Promise((resolve, reject) => {
this.auth0.client.userInfo(token, (err, user) => {
if (err) {
reject(err)
} else {
resolve(user)
}
})
})
}
handleAuthentication (store, router) {
this.parseHash()
.then(authResult => {
if (authResult && authResult.accessToken && authResult.idToken) {
this.setSession(authResult)
this.userInfo(authResult.accessToken)
.then(user => {
store.commit(mutations.USER_LOGGED_ON, user)
router.replace('/journal')
})
}
})
.catch(err => {
router.replace('/')
console.log(err)
alert(`Error: ${err.error}. Check the console for further details.`)
})
}
scheduleRenewal () {
let expiresAt = JSON.parse(localStorage.getItem('expires_at'))
let delay = expiresAt - Date.now()
if (delay > 0) {
tokenRenewalTimeout = setTimeout(() => {
this.renewToken()
}, delay)
async handleAuthentication (store) {
try {
const authResult = await this.parseHash()
if (authResult && authResult.accessToken && authResult.idToken) {
this.setSession(authResult)
store.commit(mutations.USER_LOGGED_ON, this.session.profile)
}
} catch(err) {
console.error(err)
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) {
// Set the time that the access token will expire at
let expiresAt = JSON.stringify(
authResult.expiresIn * 1000 + new Date().getTime()
)
localStorage.setItem('access_token', authResult.accessToken)
localStorage.setItem('id_token', authResult.idToken)
localStorage.setItem('expires_at', expiresAt)
this.scheduleRenewal()
this.session.profile = authResult.idTokenPayload
this.session.id.token = authResult.idToken
this.session.id.expiry = this.session.profile.exp * 1000
this.session.access.token = authResult.accessToken
this.session.access.expiry = 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 || {}
})
}
renewToken () {
console.log('attempting renewal...')
this.auth0.renewAuth(
{
audience: `https://${AUTH_CONFIG.domain}/userinfo`,
redirectUri: `${AUTH_CONFIG.appDomain}/static/silent.html`,
usePostMessage: true
},
(err, result) => {
if (err) {
console.log(err)
} else {
this.setSession(result)
/**
* 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))
: { profile: {},
id: {
token: null,
expiry: null
},
access: {
token: null,
expiry: null
}
}
}
/**
* Renew authorzation tokens with Auth0
*/
renewTokens () {
return new Promise((resolve, reject) => {
this.refreshSession()
if (this.session.id.token !== null) {
webAuth.checkSession({}, (err, authResult) => {
if (err) {
reject(err)
} else {
this.setSession(authResult)
resolve(authResult)
}
})
} else {
reject('Not logged in')
}
)
})
}
logout (store, router) {
/**
* Log out of myPrayerJournal
*
* @param store The Vuex store
*/
logout (store) {
// Clear access token and ID token from local storage
clearTimeout(tokenRenewalTimeout)
localStorage.removeItem('access_token')
localStorage.removeItem('id_token')
localStorage.removeItem('expires_at')
localStorage.setItem('user_profile', JSON.stringify({}))
// navigate to the home route
localStorage.removeItem(this.AUTH_SESSION)
this.refreshSession()
store.commit(mutations.USER_LOGGED_OFF)
router.replace('/')
webAuth.logout({
returnTo: `${AUTH_CONFIG.appDomain}/`,
clientID: AUTH_CONFIG.clientId
})
this.emit('loginEvent', { loggedIn: false })
}
/**
* Check expiration for a token (the way it's stored in the session)
*/
checkExpiry = (it) => it.token && it.expiry && Date.now() < it.expiry
/**
* Is there a user authenticated?
*/
isAuthenticated () {
// Check whether the current time is past the access token's expiry time
let expiresAt = JSON.parse(localStorage.getItem('expires_at'))
return new Date().getTime() < expiresAt
return this.checkExpiry(this.session.id)
}
/**
* Is the current access token valid?
*/
isAccessTokenValid () {
return this.checkExpiry(this.session.access)
}
/**
* Get the user's access token, renewing it if required
*/
async getAccessToken () {
if (this.isAccessTokenValid()) {
return this.session.access.token
} else {
try {
const authResult = await this.renewTokens()
return authResult.accessToken
} catch (reject) {
throw reject
}
}
}
}
export default new AuthService()

View File

@@ -1,16 +1,16 @@
<template lang="pug">
article.mpj-main-content(role='main')
md-content(role='main').mpj-main-content
page-title(title='Welcome!'
hideOnPage='true')
hideOnPage=true)
p &nbsp;
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 will also allow
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 currently in beta, but it is open and available to the general public. To get started, simply click
the &ldquo;Log On&rdquo; link above, and log on with either a Microsoft or Google account. You can also learn more
about the site at the &ldquo;Docs&rdquo; link, also above.
This site is open and available to the general public. To get started, simply click the &ldquo;Log On&rdquo; link
above, and log on with either a Microsoft or Google account. You can also learn more about the site at the
&ldquo;Docs&rdquo; link, also above.
</template>
<script>

View File

@@ -1,25 +1,24 @@
<template lang="pug">
article.mpj-main-content-wide(role='main')
md-content(role='main').mpj-main-content-wide
page-title(:title='title')
p(v-if='isLoadingJournal') Loading your prayer journal...
template(v-else)
.mpj-text-center
router-link(:to="{ name: 'EditRequest', params: { id: 'new' } }"
role='button').
#[md-icon(icon='add_box')] Add a New Request
br
.mpj-journal(v-if='journal.length > 0')
request-card(v-for='request in journal'
:key='request.requestId'
:request='request'
:events='eventBus'
:toast='toast')
p.text-center(v-else): em.
No requests found; click the &ldquo;Add a New Request&rdquo; button to add one
notes-edit(:events='eventBus'
:toast='toast')
snooze-request(:events='eventBus'
:toast='toast')
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>
@@ -36,6 +35,10 @@ import actions from '@/store/action-types'
export default {
name: 'journal',
inject: [
'messages',
'progress'
],
components: {
NotesEdit,
RequestCard,
@@ -50,23 +53,29 @@ export default {
title () {
return `${this.user.given_name}&rsquo;s Prayer Journal`
},
toast () {
return this.$parent.$refs.toast
snackbar () {
return this.$parent.$refs.snackbar
},
...mapState(['user', 'journal', 'isLoadingJournal'])
},
async created () {
await this.$store.dispatch(actions.LOAD_JOURNAL, this.$Progress)
this.toast.showToast(`Loaded ${this.journal.length} prayer requests`, { theme: 'success' })
await this.$store.dispatch(actions.LOAD_JOURNAL, this.progress)
this.messages.$emit('info', `Loaded ${this.journal.length} prayer requests`)
},
provide () {
return {
journalEvents: this.eventBus
}
}
}
</script>
<style>
.mpj-journal {
display: flex;
flex-flow: row wrap;
justify-content: center;
align-items: flex-start;
}
<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>

View File

@@ -1,15 +0,0 @@
<template lang="pug">
i.material-icons(v-html='icon')
</template>
<script>
export default {
name: 'md-icon',
props: {
icon: {
type: String,
required: true
}
}
}
</script>

View File

@@ -1,34 +1,29 @@
<template lang="pug">
nav.mpj-top-nav.mpj-bg(role='menubar')
router-link.title(:to="{ name: 'Home' }"
role='menuitem')
span(style='font-weight:100;') my
span(style='font-weight:600;') Prayer
span(style='font-weight:700;') Journal
router-link(v-if='isAuthenticated'
:to="{ name: 'Journal' }"
role='menuitem') Journal
router-link(v-if='isAuthenticated'
:to="{ name: 'ActiveRequests' }"
role='menuitem') Active
router-link(v-if='hasSnoozed'
:to="{ name: 'SnoozedRequests' }"
role='menuitem') Snoozed
router-link(v-if='isAuthenticated'
:to="{ name: 'AnsweredRequests' }"
role='menuitem') Answered
a(v-if='isAuthenticated'
href='#'
role='menuitem'
@click.stop='logOff()') Log Off
a(v-if='!isAuthenticated'
href='#'
role='menuitem'
@click.stop='logOn()') Log On
a(href='https://docs.prayerjournal.me'
target='_blank'
role='menuitem'
@click.stop='') Docs
.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'
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>
@@ -36,14 +31,10 @@ nav.mpj-top-nav.mpj-bg(role='menubar')
import { mapState } from 'vuex'
import AuthService from '@/auth/AuthService'
export default {
name: 'navigation',
data () {
return {
auth0: new AuthService()
}
return {}
},
computed: {
hasSnoozed () {
@@ -51,46 +42,18 @@ export default {
Array.isArray(this.journal) &&
this.journal.filter(req => req.snoozedUntil > Date.now()).length > 0
},
...mapState([ 'journal', 'isAuthenticated' ])
...mapState([ 'isAuthenticated', 'journal' ])
},
methods: {
logOn () {
this.auth0.login()
this.$auth.login()
},
logOff () {
this.auth0.logout(this.$store, this.$router)
this.$auth.logout(this.$store, this.$router)
},
showHelp () {
window.open('https://docs.prayerjournal.me', '_blank')
}
}
}
</script>
<style>
.mpj-top-nav {
position: fixed;
display: flex;
flex-flow: row wrap;
align-items: center;
top: 0;
left: 0;
width: 100%;
padding-left: .5rem;
min-height: 50px;
}
.mpj-top-nav a:link,
.mpj-top-nav a:visited {
text-decoration: none;
color: rgba(255, 255, 255, .75);
padding-left: 1rem;
}
.mpj-top-nav a:link.router-link-active,
.mpj-top-nav a:visited.router-link-active,
.mpj-top-nav a:hover {
color: white;
}
.mpj-top-nav .title {
font-size: 1.25rem;
color: white;
padding-left: 1.25rem;
padding-right: 1.25rem;
}
</style>

View File

@@ -1,6 +1,6 @@
<template lang="pug">
h2.mpj-page-title(v-if='!hideOnPage'
v-html='title')
h1(v-if='!hideOnPage'
v-html='title').md-title
</template>
<script>
@@ -26,10 +26,3 @@ export default {
}
}
</script>
<style scoped>
.mpj-page-title {
border-bottom: solid 1px lightgray;
margin-bottom: 20px;
}
</style>

View File

@@ -1,54 +1,59 @@
<template lang="pug">
article
page-title(title='Privacy Policy')
p: small: em (as of May 21, 2018)
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 (&ldquo;sub&rdquo;) 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 &ldquo;Log Off&rdquo; 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 &ldquo;monetize&rdquo; 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.
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 (&ldquo;sub&rdquo;) 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 &ldquo;Log Off&rdquo; 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 &ldquo;monetize&rdquo; 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>

View File

@@ -1,35 +1,40 @@
<template lang="pug">
article
page-title(title='Terms of Service')
p: small: em (as of May 21, 2018)
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.
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>

View File

@@ -1,13 +1,16 @@
<template lang="pug">
article.mpj-main-content(role='main')
page-title(title='Active Requests')
div(v-if='loaded').mpj-request-list
p.mpj-text-center(v-if='requests.length === 0'): em.
No active requests found; return to #[router-link(:to='{ name: "Journal" } ') your journal]
request-list-item(v-for='req in requests'
:key='req.requestId'
:request='req'
:toast='toast')
md-content(role='main').mpj-main-content
page-title(title='Active Requests'
hide-on-page=true)
template(v-if='loaded')
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>
@@ -16,14 +19,15 @@ article.mpj-main-content(role='main')
import { mapState } from 'vuex'
import RequestListItem from '@/components/request/RequestListItem'
import RequestList from '@/components/request/RequestList'
import actions from '@/store/action-types'
export default {
name: 'active-requests',
inject: ['progress'],
components: {
RequestListItem
RequestList
},
data () {
return {
@@ -32,9 +36,6 @@ export default {
}
},
computed: {
toast () {
return this.$parent.$refs.toast
},
...mapState(['journal', 'isLoadingJournal'])
},
created () {
@@ -45,7 +46,7 @@ export default {
async ensureJournal () {
if (!Array.isArray(this.journal)) {
this.loaded = false
await this.$store.dispatch(actions.LOAD_JOURNAL, this.$Progress)
await this.$store.dispatch(actions.LOAD_JOURNAL, this.progress)
}
this.requests = this.journal
.sort((a, b) => a.showAfter - b.showAfter)

View File

@@ -1,13 +1,15 @@
<template lang="pug">
article.mpj-main-content(role='main')
page-title(title='Answered Requests')
div(v-if='loaded').mpj-request-list
p.text-center(v-if='requests.length === 0'): em.
No answered requests found; once you have marked one as &ldquo;Answered&rdquo;, it will appear here
request-list-item(v-for='req in requests'
:key='req.requestId'
:request='req'
:toast='toast')
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>
@@ -16,12 +18,16 @@ article.mpj-main-content(role='main')
import api from '@/api'
import RequestListItem from '@/components/request/RequestListItem'
import RequestList from '@/components/request/RequestList'
export default {
name: 'answered-requests',
inject: [
'messages',
'progress'
],
components: {
RequestListItem
RequestList
},
data () {
return {
@@ -29,21 +35,16 @@ export default {
loaded: false
}
},
computed: {
toast () {
return this.$parent.$refs.toast
}
},
async mounted () {
this.$Progress.start()
this.progress.$emit('show', 'query')
try {
const reqs = await api.getAnsweredRequests()
this.requests = reqs.data
this.$Progress.finish()
this.progress.$emit('done')
} catch (err) {
console.error(err)
this.toast.showToast('Error loading requests; check console for details', { theme: 'danger' })
this.$Progress.fail()
this.messages.$emit('error', 'Error loading requests; check console for details')
this.progress.$emit('done')
} finally {
this.loaded = true
}

View File

@@ -1,71 +1,52 @@
<template lang="pug">
article.mpj-main-content(role='main')
md-content(role='main').mpj-narrow
page-title(:title='title')
.mpj-narrow
label(for='request_text')
| Prayer Request
br
textarea(v-model='form.requestText'
:rows='10'
@blur='trimText()'
autofocus).mpj-full-width
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')
label Also Mark As
br
template(v-if='!isNew')
label Also Mark As
br
label.normal
input(v-model='form.status'
type='radio'
name='status'
value='Updated')
| Updated
| &nbsp; &nbsp;
label.normal
input(v-model='form.status'
type='radio'
name='status'
value='Prayed')
| Prayed
| &nbsp; &nbsp;
label.normal
input(v-model='form.status'
type='radio'
name='status'
value='Answered')
| Answered
br
label Recurrence
| &nbsp; &nbsp;
em.mpj-muted-text After prayer, request reappears...
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.normal
input(v-model='form.recur.typ'
type='radio'
name='recur'
value='immediate')
| Immediately
| &nbsp; &nbsp;
label.normal
input(v-model='form.recur.typ'
type='radio'
name='recur'
value='other')
| Every...
input(v-model='form.recur.count'
type='number'
:disabled='!showRecurrence').mpj-recur-count
select(v-model='form.recur.other'
:disabled='!showRecurrence').mpj-recur-type
option(value='hours') hours
option(value='days') days
option(value='weeks') weeks
.mpj-text-right
button(:disabled='!isValidRecurrence'
@click.stop='saveRequest()').primary.
#[md-icon(icon='save')] Save
| &nbsp; &nbsp;
button(@click.stop='goBack()').
#[md-icon(icon='arrow_back')] Cancel
label Recurrence
| &nbsp; &nbsp;
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')
.md-layout-item.md-size-20
md-field
label Interval
md-select(v-model='form.recur.other'
:disabled='!showRecurrence')
md-option(value='Hours') hours
md-option(value='Days') days
md-option(value='Weeks') weeks
.mpj-text-right
md-button(:disabled='!isValidRecurrence'
@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>
@@ -77,6 +58,10 @@ import actions from '@/store/action-types'
export default {
name: 'edit-request',
inject: [
'messages',
'progress'
],
props: {
id: {
type: String,
@@ -92,7 +77,7 @@ export default {
requestText: '',
status: 'Updated',
recur: {
typ: 'immediate',
typ: 'Immediate',
other: '',
count: ''
}
@@ -101,19 +86,16 @@ export default {
},
computed: {
isValidRecurrence () {
if (this.form.recur.typ === 'immediate') return true
if (this.form.recur.typ === 'Immediate') return true
const count = Number.parseInt(this.form.recur.count)
if (isNaN(count) || this.form.recur.other === '') return false
if (this.form.recur.other === 'hours' && count > (365 * 24)) return false
if (this.form.recur.other === 'days' && count > 365) return false
if (this.form.recur.other === 'weeks' && count > 52) return false
if (this.form.recur.other === 'Hours' && count > (365 * 24)) return false
if (this.form.recur.other === 'Days' && count > 365) return false
if (this.form.recur.other === 'Weeks' && count > 52) return false
return true
},
showRecurrence () {
return this.form.recur.typ !== 'immediate'
},
toast () {
return this.$parent.$refs.toast
return this.form.recur.typ !== 'Immediate'
},
...mapState(['journal'])
},
@@ -125,21 +107,21 @@ export default {
this.form.requestId = ''
this.form.requestText = ''
this.form.status = 'Created'
this.form.recur.typ = 'immediate'
this.form.recur.typ = 'Immediate'
this.form.recur.other = ''
this.form.recur.count = ''
} else {
this.title = 'Edit Prayer Request'
this.isNew = false
if (this.journal.length === 0) {
await this.$store.dispatch(actions.LOAD_JOURNAL, this.$Progress)
await this.$store.dispatch(actions.LOAD_JOURNAL, this.progress)
}
const req = this.journal.filter(r => r.requestId === this.id)[0]
this.form.requestId = this.id
this.form.requestText = req.text
this.form.status = 'Updated'
if (req.recurType === 'immediate') {
this.form.recur.typ = 'immediate'
if (req.recurType === 'Immediate') {
this.form.recur.typ = 'Immediate'
this.form.recur.other = ''
this.form.recur.count = ''
} else {
@@ -158,31 +140,31 @@ export default {
},
async ensureJournal () {
if (!Array.isArray(this.journal)) {
await this.$store.dispatch(actions.LOAD_JOURNAL, this.$Progress)
await this.$store.dispatch(actions.LOAD_JOURNAL, this.progress)
}
},
async saveRequest () {
if (this.isNew) {
await this.$store.dispatch(actions.ADD_REQUEST, {
progress: this.$Progress,
progress: this.progress,
requestText: this.form.requestText,
recurType: this.form.recur.typ === 'immediate' ? 'immediate' : this.form.recur.other,
recurCount: this.form.recur.typ === 'immediate' ? 0 : Number.parseInt(this.form.recur.count)
recurType: this.form.recur.typ === 'Immediate' ? 'Immediate' : this.form.recur.other,
recurCount: this.form.recur.typ === 'Immediate' ? 0 : Number.parseInt(this.form.recur.count)
})
this.toast.showToast('New prayer request added', { theme: 'success' })
this.messages.$emit('info', 'New prayer request added')
} else {
await this.$store.dispatch(actions.UPDATE_REQUEST, {
progress: this.$Progress,
progress: this.progress,
requestId: this.form.requestId,
updateText: this.form.requestText,
status: this.form.status,
recurType: this.form.recur.typ === 'immediate' ? 'immediate' : this.form.recur.other,
recurCount: this.form.recur.typ === 'immediate' ? 0 : Number.parseInt(this.form.recur.count)
recurType: this.form.recur.typ === 'Immediate' ? 'Immediate' : this.form.recur.other,
recurCount: this.form.recur.typ === 'Immediate' ? 0 : Number.parseInt(this.form.recur.count)
})
if (this.form.status === 'Answered') {
this.toast.showToast('Request updated and removed from active journal', { theme: 'success' })
this.messages.$emit('info', 'Request updated and removed from active journal')
} else {
this.toast.showToast('Request updated', { theme: 'success' })
this.messages.$emit('info', 'Request updated')
}
}
this.goBack()
@@ -190,15 +172,3 @@ export default {
}
}
</script>
<style>
.mpj-recur-count {
width: 3rem;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.mpj-recur-type {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
</style>

View File

@@ -1,22 +1,24 @@
<template lang="pug">
article.mpj-main-content(role='main')
page-title(title='Full Prayer Request')
template(v-if='request')
p
span(v-if='isAnswered') Answered {{ formatDate(answered) }} (#[date-from-now(:value='answered')]) &nbsp;
small: em.mpj-muted-text prayed {{ prayedCount }} times, open {{ openDays }} days
p.mpj-request-text {{ lastText }}
br
table.mpj-request-log
thead
tr
th Action
th Update / Notes
tbody
tr(v-for='item in log' :key='item.asOf')
td {{ item.status }} on #[span.mpj-text-nowrap {{ formatDate(item.asOf) }}]
td(v-if='item.text').mpj-request-text {{ item.text }}
td(v-else) &nbsp;
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') Answered {{ formatDate(answered) }} (#[date-from-now(:value='answered')]) !{' &bull; '}
| Prayed {{ prayedCount }} times &bull; 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) &nbsp;
p(v-else) Loading request...
</template>
@@ -31,6 +33,7 @@ const asOfDesc = (a, b) => b.asOf - a.asOf
export default {
name: 'full-request',
inject: ['progress'],
props: {
id: {
type: String,
@@ -72,14 +75,14 @@ export default {
}
},
async mounted () {
this.$Progress.start()
this.progress.$emit('show', 'indeterminate')
try {
const req = await api.getFullRequest(this.id)
this.request = req.data
this.$Progress.finish()
this.progress.$emit('done')
} catch (e) {
console.log(e)
this.$Progress.fail()
this.progress.$emit('done')
}
},
methods: {

View File

@@ -1,21 +1,16 @@
<template lang="pug">
.mpj-modal(v-show='notesVisible')
.mpj-modal-content.mpj-narrow
header.mpj-bg
h5 Add Notes to Prayer Request
label
| Notes
br
textarea(v-model='form.notes'
:rows='10'
@blur='trimText()').mpj-full-width
.mpj-text-right
button(@click='saveNotes()').primary.
#[md-icon(icon='save')] Save
| &nbsp; &nbsp;
button(@click='closeDialog()').
#[md-icon(icon='undo')] Cancel
hr
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
.mpj-dialog-content
div(v-if='hasPriorNotes')
p.mpj-text-center: strong Prior Notes for This Request
.mpj-note-list
@@ -26,8 +21,8 @@
span.mpj-request-text {{ note.notes }}
div(v-else-if='noPriorNotes').mpj-text-center.mpj-muted-text There are no prior notes for this request
div(v-else).mpj-text-center
button(@click='loadNotes()').
#[md-icon(icon='cloud_download')] Load Prior Notes
hr
md-button(@click='loadNotes()') #[md-icon cloud_download] Load Prior Notes
</template>
<script>
@@ -37,10 +32,11 @@ import api from '@/api'
export default {
name: 'notes-edit',
props: {
toast: { required: true },
events: { required: true }
},
inject: [
'journalEvents',
'messages',
'progress'
],
data () {
return {
notesVisible: false,
@@ -61,7 +57,7 @@ export default {
}
},
created () {
this.events.$on('notes', this.openDialog)
this.journalEvents.$on('notes', this.openDialog)
},
methods: {
closeDialog () {
@@ -72,14 +68,14 @@ export default {
this.notesVisible = false
},
async loadNotes () {
this.$Progress.start()
this.progress.$emit('show', 'indeterminate')
try {
const notes = await api.getNotes(this.form.requestId)
this.priorNotes = notes.data
this.$Progress.finish()
this.progress.$emit('done')
} catch (e) {
console.error(e)
this.$Progress.fail()
this.progress.$emit('done')
} finally {
this.priorNotesLoaded = true
}
@@ -89,15 +85,15 @@ export default {
this.notesVisible = true
},
async saveNotes () {
this.$Progress.start()
this.progress.$emit('show', 'indeterminate')
try {
await api.addNote(this.form.requestId, this.form.notes)
this.$Progress.finish()
this.toast.showToast('Added notes', { theme: 'success' })
this.progress.$emit('done')
this.messages.$emit('info', 'Added notes')
this.closeDialog()
} catch (e) {
console.error(e)
this.$Progress.fail()
this.progress.$emit('done')
}
},
trimText () {
@@ -107,8 +103,16 @@ export default {
}
</script>
<style>
.mpj-note-list p {
border-top: dotted 1px lightgray;
}
<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>

View File

@@ -1,17 +1,27 @@
<template lang="pug">
.mpj-request-card(v-if='shouldDisplay')
header.mpj-card-header(role='toolbar').
#[button(@click='markPrayed()' title='Pray').primary: md-icon(icon='done')]
#[button(@click.stop='showEdit()' title='Edit'): md-icon(icon='edit')]
#[button(@click.stop='showNotes()' title='Add Notes'): md-icon(icon='comment')]
#[button(@click.stop='snooze()' title='Snooze Request'): md-icon(icon='schedule')]
div
p.card-text.mpj-request-text
| {{ request.text }}
p.as-of.mpj-text-right: small.mpj-muted-text: em
= '(last activity '
date-from-now(:value='request.asOf')
| )
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>
@@ -21,10 +31,13 @@ import actions from '@/store/action-types'
export default {
name: 'request-card',
inject: [
'journalEvents',
'messages',
'progress'
],
props: {
request: { required: true },
toast: { required: true },
events: { required: true }
request: { required: true }
},
computed: {
shouldDisplay () {
@@ -35,59 +48,31 @@ export default {
methods: {
async markPrayed () {
await this.$store.dispatch(actions.UPDATE_REQUEST, {
progress: this.$Progress,
progress: this.progress,
requestId: this.request.requestId,
status: 'Prayed',
updateText: ''
})
this.toast.showToast('Request marked as prayed', { theme: 'success' })
this.messages.$emit('info', 'Request marked as prayed')
},
showEdit () {
this.$router.push({ name: 'EditRequest', params: { id: this.request.requestId } })
},
showNotes () {
this.events.$emit('notes', this.request)
this.journalEvents.$emit('notes', this.request)
},
snooze () {
this.events.$emit('snooze', this.request.requestId)
this.journalEvents.$emit('snooze', this.request.requestId)
}
}
}
</script>
<style>
.mpj-request-card {
border: solid 1px darkgray;
border-radius: 5px;
width: 20rem;
margin: .5rem;
}
@media screen and (max-width: 20rem) {
.mpj-request-card {
width: 100%;
}
}
.mpj-card-header {
display: flex;
flex-flow: row;
justify-content: center;
background-image: -webkit-gradient(linear, left top, left bottom, from(lightgray), to(whitesmoke));
background-image: -webkit-linear-gradient(top, lightgray, whitesmoke);
background-image: -moz-linear-gradient(top, lightgray, whitesmoke);
background-image: linear-gradient(to bottom, lightgray, whitesmoke);
}
.mpj-card-header button {
margin: .25rem;
padding: 0 .25rem;
}
.mpj-card-header button .material-icons {
font-size: 1.3rem;
}
.mpj-request-card .card-text {
margin-left: 1rem;
margin-right: 1rem;
}
.mpj-request-card .as-of {
margin-right: .25rem;
}
<style lang="sass">
.mpj-request-card
width: 20rem
margin-bottom: 1rem
@media screen and (max-width: 20rem)
.mpj-request-card
width: 100%
</style>

View File

@@ -0,0 +1,40 @@
<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>
'use strict'
import RequestListItem from '@/components/request/RequestListItem'
export default {
name: 'request-list',
components: { RequestListItem },
props: {
title: {
type: String,
required: true
},
requests: {
type: Array,
required: true
}
},
data () {
return { }
},
created () {
this.$on('requestUnsnoozed', this.$parent.$emit('requestUnsnoozed'))
this.$on('requestNowShown', this.$parent.$emit('requestNowShown'))
}
}
</script>

View File

@@ -1,31 +1,31 @@
<template lang="pug">
p.mpj-request-text
| {{ request.text }}
br
br
button(@click='viewFull'
title='View Full Request').
#[md-icon(icon='description')] View Full Request
| &nbsp; &nbsp;
template(v-if='!isAnswered')
button(@click='editRequest'
title='Edit Request').
#[md-icon(icon='edit')] Edit Request
| &nbsp; &nbsp;
template(v-if='isSnoozed')
button(@click='cancelSnooze()').
#[md-icon(icon='restore')] Cancel Snooze
| &nbsp; &nbsp;
template(v-if='isPending')
button(@click='showNow()').
#[md-icon(icon='restore')] Show Now
br(v-if='isSnoozed || isPending || isAnswered')
small(v-if='isSnoozed').mpj-muted-text: em.
&nbsp; Snooze expires #[date-from-now(:value='request.snoozedUntil')]
small(v-if='isPending').mpj-muted-text: em.
&nbsp; Request scheduled to reappear #[date-from-now(:value='request.showAfter')]
small(v-if='isAnswered').mpj-muted-text: em.
&nbsp; Answered #[date-from-now(:value='request.asOf')]
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')
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')
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')
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 || isPending || isAnswered')
small(v-if='isSnoozed').mpj-muted-text: em Snooze expires #[date-from-now(:value='request.snoozedUntil')]
small(v-if='isPending').mpj-muted-text: em Request appears next #[date-from-now(:value='request.showAfter')]
small(v-if='isAnswered').mpj-muted-text: em Answered #[date-from-now(:value='request.asOf')]
</template>
<script>
@@ -35,9 +35,12 @@ import actions from '@/store/action-types'
export default {
name: 'request-list-item',
inject: [
'messages',
'progress'
],
props: {
request: { required: true },
toast: { required: true }
request: { required: true }
},
data () {
return {}
@@ -59,11 +62,11 @@ export default {
methods: {
async cancelSnooze () {
await this.$store.dispatch(actions.SNOOZE_REQUEST, {
progress: this.$Progress,
progress: this.progress,
requestId: this.request.requestId,
until: 0
})
this.toast.showToast('Request un-snoozed', { theme: 'success' })
this.messages.$emit('info', 'Request un-snoozed')
this.$parent.$emit('requestUnsnoozed')
},
editRequest () {
@@ -71,11 +74,11 @@ export default {
},
async showNow () {
await this.$store.dispatch(actions.SHOW_REQUEST_NOW, {
progress: this.$Progress,
progress: this.progress,
requestId: this.request.requestId,
showAfter: Date.now()
showAfter: 0
})
this.toast.showToast('Recurrence skipped; request now shows in journal', { theme: 'success' })
this.messages.$emit('info', 'Recurrence skipped; request now shows in journal')
this.$parent.$emit('requestNowShown')
},
viewFull () {
@@ -84,3 +87,9 @@ export default {
}
}
</script>
<style lang="sass">
.mpj-action-cell
width: 1%
white-space: nowrap
</style>

View File

@@ -1,22 +1,15 @@
<template lang="pug">
.mpj-modal(v-show='snoozeVisible')
.mpj-modal-content.mpj-skinny
header.mpj-bg
h5 Snooze Prayer Request
p.mpj-text-center
label
= 'Until '
input(v-model='form.snoozedUntil'
type='date'
autofocus)
br
.mpj-text-right
button.primary(:disabled='!isValid'
@click='snoozeRequest()').
#[md-icon(icon='snooze')] Snooze
| &nbsp; &nbsp;
button(@click='closeDialog()').
#[md-icon(icon='undo')] Cancel
md-dialog(:md-active.sync='snoozeVisible').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'
@click='snoozeRequest()').md-primary #[md-icon snooze] Snooze
md-button(@click='closeDialog()') #[md-icon undo] Cancel
</template>
<script>
@@ -26,13 +19,18 @@ import actions from '@/store/action-types'
export default {
name: 'snooze-request',
inject: [
'journalEvents',
'messages',
'progress'
],
props: {
toast: { required: true },
events: { required: true }
},
data () {
return {
snoozeVisible: false,
datesInPast: date => date < new Date(),
form: {
requestId: '',
snoozedUntil: ''
@@ -40,7 +38,7 @@ export default {
}
},
created () {
this.events.$on('snooze', this.openDialog)
this.journalEvents.$on('snooze', this.openDialog)
},
computed: {
isValid () {
@@ -59,11 +57,11 @@ export default {
},
async snoozeRequest () {
await this.$store.dispatch(actions.SNOOZE_REQUEST, {
progress: this.$Progress,
progress: this.progress,
requestId: this.form.requestId,
until: Date.parse(this.form.snoozedUntil)
})
this.toast.showToast(`Request snoozed until ${this.form.snoozedUntil}`, { theme: 'success' })
this.messages.$emit('info', `Request snoozed until ${this.form.snoozedUntil}`)
this.closeDialog()
}
}

View File

@@ -1,13 +1,16 @@
<template lang="pug">
article.mpj-main-content(role='main')
page-title(title='Snoozed Requests')
div(v-if='loaded').mpj-request-list
p.mpj-text-center(v-if='requests.length === 0'): em.
No snoozed requests found; return to #[router-link(:to='{ name: "Journal" } ') your journal]
request-list-item(v-for='req in requests'
:key='req.requestId'
:request='req'
:toast='toast')
page-title(title='Snoozed Requests'
hide-on-page=true)
template(v-if='loaded')
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>
@@ -18,12 +21,13 @@ import { mapState } from 'vuex'
import actions from '@/store/action-types'
import RequestListItem from '@/components/request/RequestListItem'
import RequestList from '@/components/request/RequestList'
export default {
name: 'snoozed-requests',
inject: ['progress'],
components: {
RequestListItem
RequestList
},
data () {
return {
@@ -32,9 +36,6 @@ export default {
}
},
computed: {
toast () {
return this.$parent.$refs.toast
},
...mapState(['journal', 'isLoadingJournal'])
},
created () {
@@ -44,7 +45,7 @@ export default {
async ensureJournal () {
if (!Array.isArray(this.journal)) {
this.loaded = false
await this.$store.dispatch(actions.LOAD_JOURNAL, this.$Progress)
await this.$store.dispatch(actions.LOAD_JOURNAL, this.progress)
}
this.requests = this.journal
.filter(req => req.snoozedUntil > Date.now())

View File

@@ -7,14 +7,17 @@ article.mpj-main-content(role='main')
<script>
'use strict'
import AuthService from '@/auth/AuthService'
export default {
name: 'log-on',
created () {
this.$Progress.start()
new AuthService().handleAuthentication(this.$store, this.$router)
// Auth service redirects to dashboard, which restarts the progress bar
inject: ['progress'],
async created () {
this.progress.$emit('show', 'indeterminate')
await this.$auth.handleAuthentication(this.$store)
},
methods: {
handleLoginEvent (data) {
this.$router.push(data.state.target || '/journal')
}
}
}
</script>

View File

@@ -1,33 +1,61 @@
/* eslint-disable */
// Vue packages and components
import Vue from 'vue'
import VueProgressBar from 'vue-progressbar'
import VueToast from 'vue-toast'
import { MdApp,
MdButton,
MdCard,
MdContent,
MdDatepicker,
MdDialog,
MdEmptyState,
MdField,
MdIcon,
MdLayout,
MdProgress,
MdRadio,
MdSnackbar,
MdTable,
MdTabs,
MdToolbar,
MdTooltip } from 'vue-material/dist/components'
import 'vue-toast/dist/vue-toast.min.css'
import App from './App'
import router from './router'
import store from './store'
// myPrayerJournal components
import App from './App'
import router from './router'
import store from './store'
import DateFromNow from './components/common/DateFromNow'
import MaterialDesignIcon from './components/common/MaterialDesignIcon'
import PageTitle from './components/common/PageTitle'
import PageTitle from './components/common/PageTitle'
import AuthPlugin from './plugins/auth'
/* eslint-enable */
// Styles
import 'vue-material/dist/vue-material.min.css'
import 'vue-material/dist/theme/default.css'
Vue.config.productionTip = false
Vue.use(VueProgressBar, {
color: 'yellow',
failedColor: 'red',
height: '5px',
transition: {
speed: '0.2s',
opacity: '0.6s',
termination: 1000
}
})
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.component('date-from-now', DateFromNow)
Vue.component('md-icon', MaterialDesignIcon)
Vue.component('page-title', PageTitle)
Vue.component('toast', VueToast)
new Vue({
router,

View File

@@ -0,0 +1,22 @@
'use strict'
import authService from '../auth/AuthService'
export default {
install (Vue) {
Vue.prototype.$auth = authService
Vue.mixin({
created () {
if (this.handleLoginEvent) {
authService.addListener('loginEvent', this.handleLoginEvent)
}
},
destroyed () {
if (this.handleLoginEvent) {
authService.removeListener('loginEvent', this.handleLoginEvent)
}
}
})
}
}

View File

@@ -1,18 +1,12 @@
'use strict'
import Vue from 'vue'
/* eslint-disable */
import Vue from 'vue'
import Router from 'vue-router'
import ActiveRequests from '@/components/request/ActiveRequests'
import AnsweredRequests from '@/components/request/AnsweredRequests'
import EditRequest from '@/components/request/EditRequest'
import FullRequest from '@/components/request/FullRequest'
import auth from './auth/AuthService'
import Home from '@/components/Home'
import Journal from '@/components/Journal'
import LogOn from '@/components/user/LogOn'
import PrivacyPolicy from '@/components/legal/PrivacyPolicy'
import SnoozedRequests from '@/components/request/SnoozedRequests'
import TermsOfService from '@/components/legal/TermsOfService'
/* eslint-enable */
Vue.use(Router)
@@ -26,6 +20,12 @@ export default new Router({
return { x: 0, y: 0 }
}
},
beforeEach (to, from, next) {
if (to.path === '/' || to.path === '/user/log-on' || auth.isAuthenticated()) {
return next()
}
auth.login({ target: to.path })
},
routes: [
{
path: '/',
@@ -35,49 +35,49 @@ export default new Router({
{
path: '/journal',
name: 'Journal',
component: Journal
component: () => import('@/components/Journal')
},
{
path: '/legal/privacy-policy',
name: 'PrivacyPolicy',
component: PrivacyPolicy
component: () => import('@/components/legal/PrivacyPolicy')
},
{
path: '/legal/terms-of-service',
name: 'TermsOfService',
component: TermsOfService
component: () => import('@/components/legal/TermsOfService')
},
{
path: '/request/:id/edit',
name: 'EditRequest',
component: EditRequest,
component: () => import('@/components/request/EditRequest'),
props: true
},
{
path: '/request/:id/full',
name: 'FullRequest',
component: FullRequest,
component: () => import('@/components/request/FullRequest'),
props: true
},
{
path: '/requests/active',
name: 'ActiveRequests',
component: ActiveRequests
component: () => import('@/components/request/ActiveRequests')
},
{
path: '/requests/answered',
name: 'AnsweredRequests',
component: AnsweredRequests
component: () => import('@/components/request/AnsweredRequests')
},
{
path: '/requests/snoozed',
name: 'SnoozedRequests',
component: SnoozedRequests
component: () => import('@/components/request/SnoozedRequests')
},
{
path: '/user/log-on',
name: 'LogOn',
component: LogOn
component: () => import('@/components/user/LogOn')
}
]
})

View File

@@ -3,6 +3,8 @@
export default {
/** Action to add a prayer request (pass request text) */
ADD_REQUEST: 'add-request',
/** Action to check if a user is authenticated, refreshing the session first if it exists */
CHECK_AUTHENTICATION: 'check-authentication',
/** Action to load the user's prayer journal */
LOAD_JOURNAL: 'load-journal',
/** Action to update a request */

View File

@@ -1,47 +1,59 @@
'use strict'
import Vue from 'vue'
/* eslint-disable no-multi-spaces */
import Vue from 'vue'
import Vuex from 'vuex'
import api from '@/api'
import AuthService from '@/auth/AuthService'
import api from '@/api'
import auth from '@/auth/AuthService'
import mutations from './mutation-types'
import actions from './action-types'
import actions from './action-types'
/* eslint-enable no-multi-spaces */
Vue.use(Vuex)
const auth0 = new AuthService()
/* eslint-disable no-console */
const logError = function (error) {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.log(error.response.data)
console.log(error.response.status)
console.log(error.response.headers)
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.log(error.request)
console.error(error.request)
} else {
// Something happened in setting up the request that triggered an Error
console.log('Error', error.message)
console.error('Error', error.message)
}
console.log(error.config)
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 === 'Not logged in') {
console.warn('API request attempted when user was not logged in')
} else {
console.error(err)
}
}
}
/* eslint-enable no-console */
export default new Vuex.Store({
state: {
user: JSON.parse(localStorage.getItem('user_profile') || '{}'),
isAuthenticated: (() => {
auth0.scheduleRenewal()
if (auth0.isAuthenticated()) {
api.setBearer(localStorage.getItem('id_token'))
}
return auth0.isAuthenticated()
})(),
user: auth.session.profile,
isAuthenticated: auth.isAuthenticated(),
journal: {},
isLoadingJournal: false
},
@@ -60,49 +72,60 @@ export default new Vuex.Store({
if (request.lastStatus !== 'Answered') jrnl.push(request)
state.journal = jrnl
},
[mutations.SET_AUTHENTICATION] (state, value) {
state.isAuthenticated = value
},
[mutations.USER_LOGGED_OFF] (state) {
state.user = {}
api.removeBearer()
state.isAuthenticated = false
},
[mutations.USER_LOGGED_ON] (state, user) {
localStorage.setItem('user_profile', JSON.stringify(user))
state.user = user
api.setBearer(localStorage.getItem('id_token'))
state.isAuthenticated = true
}
},
actions: {
async [actions.ADD_REQUEST] ({ commit }, { progress, requestText, recurType, recurCount }) {
progress.start()
progress.$emit('show', 'indeterminate')
try {
await setBearer()
const newRequest = await api.addRequest(requestText, recurType, recurCount)
commit(mutations.REQUEST_ADDED, newRequest.data)
progress.finish()
progress.$emit('done')
} catch (err) {
logError(err)
progress.fail()
progress.$emit('done')
}
},
async [actions.CHECK_AUTHENTICATION] ({ commit }) {
try {
await auth.getAccessToken()
commit(mutations.SET_AUTHENTICATION, auth.isAuthenticated())
} catch (_) {
commit(mutations.SET_AUTHENTICATION, false)
}
},
async [actions.LOAD_JOURNAL] ({ commit }, progress) {
commit(mutations.LOADED_JOURNAL, {})
progress.start()
progress.$emit('show', 'query')
commit(mutations.LOADING_JOURNAL, true)
api.setBearer(localStorage.getItem('id_token'))
await setBearer()
try {
const jrnl = await api.journal()
commit(mutations.LOADED_JOURNAL, jrnl.data)
progress.finish()
progress.$emit('done')
} catch (err) {
logError(err)
progress.fail()
progress.$emit('done')
} finally {
commit(mutations.LOADING_JOURNAL, false)
}
},
async [actions.UPDATE_REQUEST] ({ commit, state }, { progress, requestId, status, updateText, recurType, recurCount }) {
progress.start()
progress.$emit('show', 'indeterminate')
try {
await setBearer()
let oldReq = (state.journal.filter(req => req.requestId === requestId) || [])[0] || {}
if (!(status === 'Prayed' && updateText === '')) {
if (status !== 'Answered' && (oldReq.recurType !== recurType || oldReq.recurCount !== recurCount)) {
@@ -114,34 +137,36 @@ export default new Vuex.Store({
}
const request = await api.getRequest(requestId)
commit(mutations.REQUEST_UPDATED, request.data)
progress.finish()
progress.$emit('done')
} catch (err) {
logError(err)
progress.fail()
progress.$emit('done')
}
},
async [actions.SHOW_REQUEST_NOW] ({ commit }, { progress, requestId, showAfter }) {
progress.start()
progress.$emit('show', 'indeterminate')
try {
await setBearer()
await api.showRequest(requestId, showAfter)
const request = await api.getRequest(requestId)
commit(mutations.REQUEST_UPDATED, request.data)
progress.finish()
progress.$emit('done')
} catch (err) {
logError(err)
progress.fail()
progress.$emit('done')
}
},
async [actions.SNOOZE_REQUEST] ({ commit }, { progress, requestId, until }) {
progress.start()
progress.$emit('show', 'indeterminate')
try {
await setBearer()
await api.snoozeRequest(requestId, until)
const request = await api.getRequest(requestId)
commit(mutations.REQUEST_UPDATED, request.data)
progress.finish()
progress.$emit('done')
} catch (err) {
logError(err)
progress.fail()
progress.$emit('done')
}
}
},

View File

@@ -9,6 +9,8 @@ export default {
REQUEST_ADDED: 'request-added',
/** Mutation to replace a prayer request at the top of the current journal */
REQUEST_UPDATED: 'request-updated',
/** Mutation for setting the authentication state */
SET_AUTHENTICATION: 'set-authentication',
/** Mutation for logging a user off */
USER_LOGGED_OFF: 'user-logged-off',
/** Mutation for logging a user on (pass user) */

View File

@@ -1,9 +1,16 @@
const webpack = require('webpack')
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
outputDir: '../api/MyPrayerJournal.Api/wwwroot',
outputDir: '../MyPrayerJournal.Api/wwwroot',
configureWebpack: {
plugins: [
// new BundleAnalyzerPlugin(),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
]
],
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
}

File diff suppressed because it is too large Load Diff