Version 3 #67

Merged
danieljsummers merged 53 commits from version-3 into master 2021-10-26 23:39:59 +00:00
16 changed files with 325 additions and 194 deletions
Showing only changes of commit 23c6bc1f1f - Show all commits

View File

@ -13,6 +13,7 @@
"vue": "vue-cli-service build --modern && cd ../MyPrayerJournal.Api && dotnet run" "vue": "vue-cli-service build --modern && cd ../MyPrayerJournal.Api && dotnet run"
}, },
"dependencies": { "dependencies": {
"@vue/composition-api": "^0.3.2",
"auth0-js": "^9.7.3", "auth0-js": "^9.7.3",
"axios": "^0.19.0", "axios": "^0.19.0",
"core-js": "^3.3.2", "core-js": "^3.3.2",
@ -24,6 +25,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/auth0-js": "^9.10.6", "@types/auth0-js": "^9.10.6",
"@types/node": "^12.12.12",
"@typescript-eslint/eslint-plugin": "^2.8.0", "@typescript-eslint/eslint-plugin": "^2.8.0",
"@typescript-eslint/parser": "^2.8.0", "@typescript-eslint/parser": "^2.8.0",
"@vue/cli-plugin-babel": "^4.0.5", "@vue/cli-plugin-babel": "^4.0.5",

View File

@ -10,13 +10,13 @@
span(style='font-weight:700;') Journal span(style='font-weight:700;') Journal
navigation navigation
md-app-content md-app-content
md-progress-bar(v-if='progress.visible' md-progress-bar(v-if='progress.visible.value'
:md-mode='progress.mode') :md-mode='progress.mode.value')
router-view router-view
md-snackbar(:md-active.sync='snackbar.visible' md-snackbar(:md-active.sync='snackbar.visible.value'
md-position='center' md-position='center'
:md-duration='snackbar.interval' :md-duration='snackbar.interval.value'
ref='snackbar') {{ snackbar.message }} ref='snackbar') {{ snackbar.message.value }}
footer footer
p.mpj-muted-text.mpj-text-right p.mpj-muted-text.mpj-text-right
| myPrayerJournal v{{ version }} | myPrayerJournal v{{ version }}
@ -28,82 +28,119 @@
#[a(href='https://bitbadger.solutions' target='_blank') Bit Badger Solutions] #[a(href='https://bitbadger.solutions' target='_blank') Bit Badger Solutions]
</template> </template>
<script> <script lang="ts">
'use strict'
import Vue from 'vue' import Vue from 'vue'
import { computed, ref, onMounted, provide } from '@vue/composition-api'
import Navigation from '@/components/common/Navigation' import Navigation from '@/components/common/Navigation.vue'
import { Actions } from '@/store/types' import auth from './auth'
import { version } from '../package.json' import router from './router'
import store from './store'
import { Actions } from './store/types'
import { provideAuth } from './plugins/auth'
import { provideRouter } from './plugins/router'
import { provideStore } from './plugins/store'
// import { version } = require('../package.json')
function useSnackbar () {
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 useProgress () {
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
}
}
export default { export default {
name: 'app', name: 'app',
components: { components: {
Navigation Navigation
}, },
data () { 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 = useProgress()
const snackbar = useSnackbar()
onMounted(async () => store.dispatch(Actions.CheckAuthentication))
const SnackbarSymbol = Symbol('Snackbar events')
provide(SnackbarSymbol, snackbar.events)
const ProgressSymbol = Symbol('Progress events')
provide(ProgressSymbol, progress.events)
return { return {
progress: { version,
events: new Vue(), progress,
visible: false, snackbar
mode: 'query'
},
snackbar: {
events: new Vue(),
visible: false,
message: '',
interval: 4000
}
}
},
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.CheckAuthentication)
},
computed: {
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
} }
} }
} }

View File

@ -20,7 +20,7 @@ const webAuth = new auth0.WebAuth({
/** /**
* A class to handle all authentication calls and determinations * A class to handle all authentication calls and determinations
*/ */
class AuthService extends EventEmitter { export class AuthService extends EventEmitter {
// Local storage key for our session data // Local storage key for our session data
AUTH_SESSION = 'auth-session' AUTH_SESSION = 'auth-session'
@ -34,8 +34,9 @@ class AuthService extends EventEmitter {
/** /**
* Starts the user log in flow * Starts the user log in flow
* @param customState Application state to be returned after user has authenticated
*/ */
login (customState: any) { login (customState?: any) {
webAuth.authorize({ webAuth.authorize({
appState: customState appState: customState
}) })
@ -43,6 +44,7 @@ class AuthService extends EventEmitter {
/** /**
* Promisified parseHash function * Promisified parseHash function
* @returns A promise that resolves with the parsed hash returned from Auth0
*/ */
parseHash (): Promise<Auth0DecodedHash> { parseHash (): Promise<Auth0DecodedHash> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -103,6 +105,7 @@ class AuthService extends EventEmitter {
/** /**
* Renew authorzation tokens with Auth0 * Renew authorzation tokens with Auth0
* @returns A promise with the parsed hash from the Auth0 response
*/ */
renewTokens (): Promise<Auth0DecodedHash> { renewTokens (): Promise<Auth0DecodedHash> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -141,22 +144,32 @@ class AuthService extends EventEmitter {
this.emit('loginEvent', { loggedIn: false }) 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? * Is there a user authenticated?
* @returns True if a user is authenticated
*/ */
isAuthenticated () { isAuthenticated () {
return this.session && this.session.id && this.session.id.isValid() return this.session && this.session.id && this.isTokenValid(this.session.id)
} }
/** /**
* Is the current access token valid? * Is the current access token valid?
* @returns True if the user's access token is valid
*/ */
isAccessTokenValid () { isAccessTokenValid () {
return this.session && this.session.access && this.session.access.isValid() return this.session && this.session.access && this.isTokenValid(this.session.access)
} }
/** /**
* Get the user's access token, renewing it if required * 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> { async getAccessToken (): Promise<string> {
if (this.isAccessTokenValid()) { if (this.isAccessTokenValid()) {

View File

@ -6,11 +6,6 @@ export class Token {
* @param expiry The expiration for the token * @param expiry The expiration for the token
*/ */
constructor (public token: string, public expiry: number) { } // eslint-disable-line no-useless-constructor constructor (public token: string, public expiry: number) { } // eslint-disable-line no-useless-constructor
/** Whether this token is currently valid */
isValid (): boolean {
return this.token !== '' && this.expiry !== 0 && Date.now() < this.expiry
}
} }
/** A user's current session */ /** A user's current session */

View File

@ -12,11 +12,3 @@ md-content(role='main').mpj-main-content
above, and log on with either a Microsoft or Google account. You can also learn more about the site at the 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. &ldquo;Docs&rdquo; link, also above.
</template> </template>
<script>
'use strict'
export default {
name: 'home'
}
</script>

View File

@ -1,6 +1,5 @@
<script> <script lang="ts">
'use strict' import { computed, createElement, onBeforeUnmount, onMounted, ref } from '@vue/composition-api'
import moment from 'moment' import moment from 'moment'
export default { export default {
@ -19,35 +18,32 @@ export default {
default: 10000 default: 10000
} }
}, },
data () { setup (props: any) {
return { /** Interval ID for updating relative time */
fromNow: moment(this.value).fromNow(), let intervalId: number = 0
intervalId: null
/** The relative time string */
const fromNow = ref(moment(props.value).fromNow())
/** The actual date/time (used as the title for the relative time) */
const actual = computed(() => moment(props.value).format('LLLL'))
/** Update the relative time string if it is different */
const updateFromNow = () => {
const newFromNow = moment(props.value).fromNow()
if (newFromNow !== fromNow.value) fromNow.value = newFromNow
} }
},
computed: { /** Refresh the relative time string to keep it accurate */
actual () { onMounted(() => { intervalId = setInterval(updateFromNow, props.interval) })
return moment(this.value).format('LLLL')
} /** Cancel refreshing the time string represented with this component */
}, onBeforeUnmount(() => clearInterval(intervalId))
mounted () {
this.intervalId = setInterval(this.updateFromNow, this.interval) return () => createElement(props.tag, {
this.$watch('value', this.updateFromNow)
},
beforeDestroy () {
clearInterval(this.intervalId)
},
methods: {
updateFromNow () {
const newFromNow = moment(this.value).fromNow()
if (newFromNow !== this.fromNow) this.fromNow = newFromNow
}
},
render (createElement) {
return createElement(this.tag, {
domProps: { domProps: {
title: this.actual, title: actual.value,
innerText: this.fromNow innerText: fromNow.value
} }
}) })
} }

View File

@ -6,7 +6,7 @@
to='/journal') to='/journal')
md-tab(md-label='Active' md-tab(md-label='Active'
to='/requests/active') to='/requests/active')
md-tab(v-if='hasSnoozed' md-tab(v-if='hasSnoozed.value'
md-label='Snoozed' md-label='Snoozed'
to='/requests/snoozed') to='/requests/snoozed')
md-tab(md-label='Answered' md-tab(md-label='Answered'
@ -26,33 +26,48 @@
@click.prevent='showHelp()') @click.prevent='showHelp()')
</template> </template>
<script> <script lang="ts">
'use strict' import { computed } from '@vue/composition-api'
import { Store } from 'vuex' // eslint-disable-line no-unused-vars
import { mapState } from 'vuex' 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 { export default {
name: 'navigation', setup () {
data () { /** The Vuex store */
return {} const store = useStore() as Store<AppState>
},
computed: { /** The auth service */
hasSnoozed () { const auth = useAuth() as AuthService
return this.isAuthenticated &&
Array.isArray(this.journal) && /** Whether the user has any snoozed requests */
this.journal.filter(req => req.snoozedUntil > Date.now()).length > 0 const hasSnoozed = computed(() =>
}, store.state.isAuthenticated &&
...mapState(['isAuthenticated', 'journal']) Array.isArray(store.state.journal) &&
}, store.state.journal.filter(req => req.snoozedUntil > Date.now()).length > 0)
methods: {
logOn () { /** Log a user on using Auth0 */
this.$auth.login() const logOn = () => auth.login()
},
logOff () { /** Log a user off using Auth0 */
this.$auth.logout(this.$store, this.$router) const logOff = () => {
}, auth.logout(store)
showHelp () { const router = useRouter()
window.open('https://docs.prayerjournal.me', '_blank') router.push('/')
}
/** Open a new window/tab with help */
const showHelp = () => { window.open('https://docs.prayerjournal.me', '_blank') }
return {
hasSnoozed,
logOn,
logOff,
showHelp
} }
} }
} }

View File

@ -3,9 +3,10 @@ h1(v-if='!hideOnPage'
v-html='title').md-title v-html='title').md-title
</template> </template>
<script> <script lang="ts">
import { watch } from '@vue/composition-api'
export default { export default {
name: 'page-title',
props: { props: {
title: { title: {
type: String, type: String,
@ -16,13 +17,11 @@ export default {
default: false default: false
} }
}, },
watch: { setup (props: any) {
title () { watch(props.title, (title, prevTitle) => {
document.title = `${this.title.replace('&rsquo;', "'")} « myPrayerJournal` document.title = `${props.title.replace('&rsquo;', "'")} « myPrayerJournal`
} })
}, return { }
created () {
document.title = `${this.title.replace('&rsquo;', "'")} « myPrayerJournal`
} }
} }
</script> </script>

View File

@ -4,20 +4,37 @@ article.mpj-main-content(role='main')
p Logging you on... p Logging you on...
</template> </template>
<script> <script lang="ts">
'use strict' import { 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 { export default {
name: 'log-on', setup () {
inject: ['progress'], /** Auth service instance */
async created () { const auth = useAuth() as AuthService
this.progress.$emit('show', 'indeterminate')
await this.$auth.handleAuthentication(this.$store) /** Store instance */
}, const store = useStore() as Store<AppState>
methods: {
handleLoginEvent (data) { /** Router instance */
this.$router.push(data.state.target || '/journal') 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> </script>

View File

@ -1,8 +1,8 @@
/* eslint-disable */
// Vue packages and components // Vue packages and components
import Vue from 'vue' import Vue from 'vue'
import { MdApp, import VueCompositionApi from '@vue/composition-api'
import {
MdApp,
MdButton, MdButton,
MdCard, MdCard,
MdContent, MdContent,
@ -18,7 +18,8 @@ import { MdApp,
MdTable, MdTable,
MdTabs, MdTabs,
MdToolbar, MdToolbar,
MdTooltip } from 'vue-material/dist/components' MdTooltip
} from 'vue-material/dist/components'
// myPrayerJournal components // myPrayerJournal components
import App from './App.vue' import App from './App.vue'
@ -28,8 +29,6 @@ import DateFromNow from './components/common/DateFromNow.vue'
import PageTitle from './components/common/PageTitle.vue' import PageTitle from './components/common/PageTitle.vue'
import AuthPlugin from './plugins/auth' import AuthPlugin from './plugins/auth'
/* eslint-enable */
// Styles // Styles
import 'vue-material/dist/vue-material.min.css' import 'vue-material/dist/vue-material.min.css'
import 'vue-material/dist/theme/default.css' import 'vue-material/dist/theme/default.css'
@ -54,6 +53,7 @@ Vue.use(MdTabs)
Vue.use(MdToolbar) Vue.use(MdToolbar)
Vue.use(MdTooltip) Vue.use(MdTooltip)
Vue.use(AuthPlugin) Vue.use(AuthPlugin)
Vue.use(VueCompositionApi)
Vue.component('date-from-now', DateFromNow) Vue.component('date-from-now', DateFromNow)
Vue.component('page-title', PageTitle) Vue.component('page-title', PageTitle)

View File

@ -1,7 +1,8 @@
import authService from '@/auth' import { inject, provide } from '@vue/composition-api'
import authService, { AuthService } from '@/auth'
export default { export default {
install (Vue) { install (Vue: any) {
Vue.prototype.$auth = authService Vue.prototype.$auth = authService
Vue.mixin({ Vue.mixin({
@ -18,3 +19,18 @@ export default {
}) })
} }
} }
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
}

View File

@ -0,0 +1,16 @@
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
}

View File

@ -0,0 +1,20 @@
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
src/app/src/untyped-modules.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'vue-material/dist/components'

View File

@ -8,8 +8,8 @@
"moduleResolution": "node", "moduleResolution": "node",
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"sourceMap": true, "sourceMap": true,
"noImplicitAny": false,
"baseUrl": ".", "baseUrl": ".",
"types": [ "types": [
"webpack-env" "webpack-env"

View File

@ -837,6 +837,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.11.tgz#bec2961975888d964196bf0016a2f984d793d3ce" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.11.tgz#bec2961975888d964196bf0016a2f984d793d3ce"
integrity sha512-O+x6uIpa6oMNTkPuHDa9MhMMehlxLAd5QcOvKRjAFsBVpeFWTOPnXbDvILvFgFFZfQ1xh1EZi1FbXxUix+zpsQ== integrity sha512-O+x6uIpa6oMNTkPuHDa9MhMMehlxLAd5QcOvKRjAFsBVpeFWTOPnXbDvILvFgFFZfQ1xh1EZi1FbXxUix+zpsQ==
"@types/node@^12.12.12":
version "12.12.12"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.12.tgz#529bc3e73dbb35dd9e90b0a1c83606a9d3264bdb"
integrity sha512-MGuvYJrPU0HUwqF7LqvIj50RZUX23Z+m583KBygKYUZLlZ88n6w28XRNJRJgsHukLEnLz6w6SvxZoLgbr5wLqQ==
"@types/normalize-package-data@^2.4.0": "@types/normalize-package-data@^2.4.0":
version "2.4.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
@ -1129,6 +1134,13 @@
source-map "~0.6.1" source-map "~0.6.1"
vue-template-es2015-compiler "^1.9.0" vue-template-es2015-compiler "^1.9.0"
"@vue/composition-api@^0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@vue/composition-api/-/composition-api-0.3.2.tgz#2d797028e489bf7812f08c7bb33ffd03ef23c617"
integrity sha512-fD4dn9cJX62QSP2TMFLXCOQOa+Bu2o7kWDjrU/FNLkNqPPcCKBLxCH/Lc+gNCRBKdEUGyI3arjAw7j0Yz1hnvw==
dependencies:
tslib "^1.9.3"
"@vue/eslint-config-standard@^5.0.0": "@vue/eslint-config-standard@^5.0.0":
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/@vue/eslint-config-standard/-/eslint-config-standard-5.0.0.tgz#2ffabb4056205a86782cd5a641cdbcd330d905b4" resolved "https://registry.yarnpkg.com/@vue/eslint-config-standard/-/eslint-config-standard-5.0.0.tgz#2ffabb4056205a86782cd5a641cdbcd330d905b4"
@ -8789,7 +8801,7 @@ ts-loader@^6.2.0:
micromatch "^4.0.0" micromatch "^4.0.0"
semver "^6.0.0" semver "^6.0.0"
tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
version "1.10.0" version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==