diff --git a/bcrs-ui/.browserslistrc b/bcrs-ui/.browserslistrc new file mode 100644 index 0000000..8f96043 --- /dev/null +++ b/bcrs-ui/.browserslistrc @@ -0,0 +1,3 @@ +> 1% +last 2 versions +not ie <= 10 diff --git a/bcrs-ui/.editorconfig b/bcrs-ui/.editorconfig new file mode 100644 index 0000000..f6141b6 --- /dev/null +++ b/bcrs-ui/.editorconfig @@ -0,0 +1,10 @@ +[*.{js,jsx,ts,tsx,vue}] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 120 + +[{Makefile,**.mk}] +# Use tabs for indentation (Makefiles require tabs) +indent_style = tab diff --git a/bcrs-ui/.env b/bcrs-ui/.env new file mode 100644 index 0000000..638e90d --- /dev/null +++ b/bcrs-ui/.env @@ -0,0 +1,2 @@ +VUE_APP_PATH = basePath +VUE_APP_AUTH_PATH = basePath/auth diff --git a/bcrs-ui/.env.production b/bcrs-ui/.env.production new file mode 100644 index 0000000..638e90d --- /dev/null +++ b/bcrs-ui/.env.production @@ -0,0 +1,2 @@ +VUE_APP_PATH = basePath +VUE_APP_AUTH_PATH = basePath/auth diff --git a/bcrs-ui/.eslintrc.js b/bcrs-ui/.eslintrc.js new file mode 100644 index 0000000..2dbb94e --- /dev/null +++ b/bcrs-ui/.eslintrc.js @@ -0,0 +1,20 @@ +module.exports = { + root: true, + env: { + node: true + }, + extends: [ + 'plugin:vue/essential', + '@vue/standard', + '@vue/typescript' + ], + rules: { + 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', + 'max-len': ['warn', { code: 120 }] + }, + parserOptions: { + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'] + } +} diff --git a/bcrs-ui/.gitignore b/bcrs-ui/.gitignore new file mode 100644 index 0000000..a0dddc6 --- /dev/null +++ b/bcrs-ui/.gitignore @@ -0,0 +1,21 @@ +.DS_Store +node_modules +/dist + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/bcrs-ui/README.md b/bcrs-ui/README.md new file mode 100644 index 0000000..ec04c14 --- /dev/null +++ b/bcrs-ui/README.md @@ -0,0 +1,34 @@ +# hello-world + +## Project setup +``` +npm install +``` + +### Compiles and hot-reloads for development +``` +npm run serve +``` + +### Compiles and minifies for production +``` +npm run build +``` + +### Run your tests +``` +npm run test +``` + +### Lints and fixes files +``` +npm run lint +``` + +### Run your unit tests +``` +npm run test:unit +``` + +### Customize configuration +See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/bcrs-ui/babel.config.js b/bcrs-ui/babel.config.js new file mode 100644 index 0000000..e955840 --- /dev/null +++ b/bcrs-ui/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + '@vue/cli-plugin-babel/preset' + ] +} diff --git a/bcrs-ui/jest.config.js b/bcrs-ui/jest.config.js new file mode 100644 index 0000000..8b359f8 --- /dev/null +++ b/bcrs-ui/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel' +} diff --git a/bcrs-ui/package.json b/bcrs-ui/package.json new file mode 100644 index 0000000..64d79c6 --- /dev/null +++ b/bcrs-ui/package.json @@ -0,0 +1,46 @@ +{ + "name": "hello-world", + "version": "0.1.0", + "private": true, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "vue-cli-service lint", + "test:unit": "vue-cli-service test:unit --testPathPattern=." + }, + "dependencies": { + "@mdi/font": "^4.5.95", + "axios": "^0.18.0", + "core-js": "^3.4.3", + "css-loader": "^0.28.11", + "keycloak-js": "^6.0.0", + "regenerator-runtime": "^0.13.3", + "sbc-common-components": "2.0.29", + "vue": "^2.6.10", + "vuex-class": "^0.3.2", + "vue-class-component": "^7.0.2", + "vue-property-decorator": "^8.3.0", + "vue-router": "^3.1.3", + "vuetify": "^2.0.11", + "vuex": "^3.1.2" + }, + "devDependencies": { + "@types/jest": "^24.0.19", + "@typescript-eslint/parser": "^2.3.1", + "@vue/cli-plugin-eslint": "^4.1.1", + "@vue/cli-plugin-babel": "^4.1.0", + "@vue/cli-plugin-typescript": "^4.1.0", + "@vue/cli-plugin-unit-jest": "^4.1.0", + "@typescript-eslint/eslint-plugin": "^2.3.1", + "@vue/eslint-config-standard": "^4.0.0", + "@vue/eslint-config-typescript": "^4.0.0", + "@vue/cli-service": "^4.1.0", + "@vue/test-utils": "1.0.0-beta.29", + "eslint": "^5.16.0", + "eslint-plugin-vue": "^5.2.3", + "sass": "^1.22.10", + "sass-loader": "^7.3.1", + "typescript": "~3.5.3", + "vue-template-compiler": "^2.6.10" + } +} diff --git a/bcrs-ui/public/config/configuration.json b/bcrs-ui/public/config/configuration.json new file mode 100644 index 0000000..4596eab --- /dev/null +++ b/bcrs-ui/public/config/configuration.json @@ -0,0 +1,6 @@ +{ + "API_URL": "https://legal-api-dev.pathfinder.gov.bc.ca/api/v1/businesses/", + "AUTH_API_URL": "https://auth-api-dev.pathfinder.gov.bc.ca/api/v1/", + "PAY_API_URL": "https://pay-api-dev.pathfinder.gov.bc.ca/api/v1/", + "KEYCLOAK_CONFIG_URL": "/local-keycloak-config-url/keycloak.json" +} diff --git a/bcrs-ui/public/favicon.png b/bcrs-ui/public/favicon.png new file mode 100644 index 0000000..4d2ffb7 Binary files /dev/null and b/bcrs-ui/public/favicon.png differ diff --git a/bcrs-ui/public/index.html b/bcrs-ui/public/index.html new file mode 100644 index 0000000..47081f6 --- /dev/null +++ b/bcrs-ui/public/index.html @@ -0,0 +1,19 @@ + + + + + + + hello-world + + + + + + +
+ + + diff --git a/bcrs-ui/src/App.vue b/bcrs-ui/src/App.vue new file mode 100644 index 0000000..16fbc64 --- /dev/null +++ b/bcrs-ui/src/App.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/bcrs-ui/src/assets/favicon.png b/bcrs-ui/src/assets/favicon.png new file mode 100644 index 0000000..4d2ffb7 Binary files /dev/null and b/bcrs-ui/src/assets/favicon.png differ diff --git a/bcrs-ui/src/assets/fonts/BCSans/BCSans-Bold.woff b/bcrs-ui/src/assets/fonts/BCSans/BCSans-Bold.woff new file mode 100644 index 0000000..f2ecf16 Binary files /dev/null and b/bcrs-ui/src/assets/fonts/BCSans/BCSans-Bold.woff differ diff --git a/bcrs-ui/src/assets/fonts/BCSans/BCSans-Bold.woff2 b/bcrs-ui/src/assets/fonts/BCSans/BCSans-Bold.woff2 new file mode 100644 index 0000000..681df5b Binary files /dev/null and b/bcrs-ui/src/assets/fonts/BCSans/BCSans-Bold.woff2 differ diff --git a/bcrs-ui/src/assets/fonts/BCSans/BCSans-BoldItalic.woff b/bcrs-ui/src/assets/fonts/BCSans/BCSans-BoldItalic.woff new file mode 100644 index 0000000..9a3353c Binary files /dev/null and b/bcrs-ui/src/assets/fonts/BCSans/BCSans-BoldItalic.woff differ diff --git a/bcrs-ui/src/assets/fonts/BCSans/BCSans-BoldItalic.woff2 b/bcrs-ui/src/assets/fonts/BCSans/BCSans-BoldItalic.woff2 new file mode 100644 index 0000000..d230235 Binary files /dev/null and b/bcrs-ui/src/assets/fonts/BCSans/BCSans-BoldItalic.woff2 differ diff --git a/bcrs-ui/src/assets/fonts/BCSans/BCSans-Italic.woff b/bcrs-ui/src/assets/fonts/BCSans/BCSans-Italic.woff new file mode 100644 index 0000000..fb061a3 Binary files /dev/null and b/bcrs-ui/src/assets/fonts/BCSans/BCSans-Italic.woff differ diff --git a/bcrs-ui/src/assets/fonts/BCSans/BCSans-Italic.woff2 b/bcrs-ui/src/assets/fonts/BCSans/BCSans-Italic.woff2 new file mode 100644 index 0000000..50baec2 Binary files /dev/null and b/bcrs-ui/src/assets/fonts/BCSans/BCSans-Italic.woff2 differ diff --git a/bcrs-ui/src/assets/fonts/BCSans/BCSans-Regular.woff b/bcrs-ui/src/assets/fonts/BCSans/BCSans-Regular.woff new file mode 100644 index 0000000..07f8f0b Binary files /dev/null and b/bcrs-ui/src/assets/fonts/BCSans/BCSans-Regular.woff differ diff --git a/bcrs-ui/src/assets/fonts/BCSans/BCSans-Regular.woff2 b/bcrs-ui/src/assets/fonts/BCSans/BCSans-Regular.woff2 new file mode 100644 index 0000000..056d2cc Binary files /dev/null and b/bcrs-ui/src/assets/fonts/BCSans/BCSans-Regular.woff2 differ diff --git a/bcrs-ui/src/assets/fonts/BCSans/LICENSE_OFL.txt b/bcrs-ui/src/assets/fonts/BCSans/LICENSE_OFL.txt new file mode 100644 index 0000000..1e5c88b --- /dev/null +++ b/bcrs-ui/src/assets/fonts/BCSans/LICENSE_OFL.txt @@ -0,0 +1,94 @@ +Copyright (c) 2015, Google Inc., copyright (c) 2019, Province of B.C. 2019 + +This Font Software is licensed under the SIL Open Font License, +Version 1.1. + +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font +creation efforts of academic and linguistic communities, and to +provide a free and open framework in which fonts may be shared and +improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply to +any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software +components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, +deleting, or substituting -- in part or in whole -- any of the +components of the Original Version, by changing formats or by porting +the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, +modify, redistribute, and sell modified and unmodified copies of the +Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in +Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the +corresponding Copyright Holder. This restriction only applies to the +primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created using +the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/bcrs-ui/src/assets/images/gov3_bc_logo.png b/bcrs-ui/src/assets/images/gov3_bc_logo.png new file mode 100644 index 0000000..a788405 Binary files /dev/null and b/bcrs-ui/src/assets/images/gov3_bc_logo.png differ diff --git a/bcrs-ui/src/assets/images/icons/file-pdf-outline.svg b/bcrs-ui/src/assets/images/icons/file-pdf-outline.svg new file mode 100644 index 0000000..6cf9d81 --- /dev/null +++ b/bcrs-ui/src/assets/images/icons/file-pdf-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/bcrs-ui/src/assets/images/icons/pdf-box.svg b/bcrs-ui/src/assets/images/icons/pdf-box.svg new file mode 100644 index 0000000..d5d0f95 --- /dev/null +++ b/bcrs-ui/src/assets/images/icons/pdf-box.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/bcrs-ui/src/assets/logo.png b/bcrs-ui/src/assets/logo.png new file mode 100644 index 0000000..f3d2503 Binary files /dev/null and b/bcrs-ui/src/assets/logo.png differ diff --git a/bcrs-ui/src/assets/styles/base.scss b/bcrs-ui/src/assets/styles/base.scss new file mode 100644 index 0000000..98b40b0 --- /dev/null +++ b/bcrs-ui/src/assets/styles/base.scss @@ -0,0 +1,264 @@ +@import "theme.scss"; + +@font-face { + font-family: 'BCSans'; + font-style: normal; + font-weight: 400; + src: url('../fonts/BCSans/BCSans-Regular.woff2') format('woff2'), /* Optimized for very modern browsers */ + url('../fonts/BCSans/BCSans-Regular.woff') format('woff'); /* Modern Browsers */ +} +@font-face { + font-family: 'BCSans'; + font-style: italic; + font-weight: 400; + src: url('../fonts/BCSans/BCSans-Italic.woff2') format('woff2'), /* Optimized for very modern browsers */ + url('../fonts/BCSans/BCSans-Italic.woff') format('woff'); /* Modern Browsers */ +} +@font-face { + font-family: 'BCSans'; + font-style: normal; + font-weight: 700; + src: url('../fonts/BCSans/BCSans-Bold.woff2') format('woff2'), /* Modern Browsers */ + url('../fonts/BCSans/BCSans-Bold.woff') format('woff'); /* Optimized for very modern browsers */ +} +@font-face { + font-family: 'BCSans'; + font-style: italic; + font-weight: 700; + src: url('../fonts/BCSans/BCSans-BoldItalic.woff2') format('woff2'), /* Modern Browsers */ + url('../fonts/BCSans/BCSans-BoldItalic.woff') format('woff'); /* Optimized for very modern browsers */ +} + +html, +body { + min-height: 100vh; +} + +html { + // default font size (14px) + font-size: 0.875rem; +} + +@media (min-width: 960px) { + html { + // default font size (16px) + font-size: 1rem; + } +} + +body { + font-size: 1rem; +} + +h1, h2, h3, h4 { + color: $gray9; +} + +h1, h2, h3 { + letter-spacing: -0.04rem; + font-weight: 700; +} + +h1 { + line-height: 2.25rem; + font-size: 2rem; +} + +p { + font-weight: 400; +} + +.v-application { + font-family: 'BCSans', Verdana, Arial, sans-serif; // BC Government Font +} + +.theme--light.v-application { + color: $gray7; + background-color: $gray1; +} + +.form-input { + display: flex; + border: 1px solid $gray5; + width: 10rem; + + .prefix { + flex: 0 0 auto; + width: 2.5rem; + padding: 0.5rem; + background-color: $gray1; + border-right: 1px solid $gray5; + text-align: center; + } + + input, + select { + padding-left: 0.5rem; + height: 36px; + } + + input { + flex: 1 1 auto; + width: 100%; + } + + select { + flex: 1 1 auto; + -webkit-appearance: menulist; + } +} + +.no-wrap { + white-space: nowrap; +} + +.pre-wrap { + white-space: pre-wrap; +} + +.align-right { + text-align: right !important; +} + +// Loading Indicator +.loading-container { + display: flex; + align-items: center; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 999; + background: rgba(255, 255, 255, 1); +} + +.loading__content { + margin-top: -2rem; + flex: 1 1 auto; + text-align: center; +} + +.loading-msg { + margin-top: 1rem; + font-weight: 600; +} + +.fade-out { + animation: fadeOut 2s forwards; +} + +// styles for 'fade leave' transition +// NB: 'fade enter' looks better without transition +// .fade-enter-active, +.fade-leave-active { + transition: opacity .5s; +} +// .fade-enter, +.fade-leave-to { + opacity: 0; +} + +@keyframes fadeOut { + 0% { + opacity: 1; + } + 80% { + opacity: 1; + display: block; + } + 100% { + opacity: 0; + display: none; + z-index: -5; + } +} + +// Common +ul.list { + margin: 0; + padding: 0; + list-style-type: none; +} + +.list-item { + display: flex; + flex-direction: row; + align-items: center; + padding: 1rem 1.25rem; + line-height: 1.25rem; +} + +.list-item + .list-item { + border-top: 1px solid $gray3; +} + +.list-item__icon { + margin-right: 0.5rem; +} + +.list-item__title { + font-weight: 700; +} + +.list-item__subtitle { + color: $gray6; +} + +.list-item__actions { + flex: 0 0 auto; + margin-left: auto; + + .v-btn { + margin: 0; + min-width: 8rem; + font-weight: 500; + } +} + +// No Results +.no-results { + text-align: center; +} + +.no-results__title { + color: $gray8; + font-size: 1rem; + font-weight: 700; +} + +.no-results__subtitle { + margin-top: 0.25rem; + color: $gray6; + font-size: 0.875rem; + font-weight: 500; +} + +.gray6 { + color: $gray6; +} + +.genErr { + font-size: 0.9rem; +} + +.white-background { + background-color: white !important; +} + +// Hides an element to all devices except screen readers +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +// Vue Affix +.vue-affix { + width: 282px; +} diff --git a/bcrs-ui/src/assets/styles/layout.scss b/bcrs-ui/src/assets/styles/layout.scss new file mode 100644 index 0000000..b19ac9a --- /dev/null +++ b/bcrs-ui/src/assets/styles/layout.scss @@ -0,0 +1,74 @@ +.app-container { + display: flex; + flex-flow: column nowrap; + min-height: 100vh; +} + +.app-header { + position: -webkit-sticky; // Safari + position: sticky; + top: 0; + width: 100%; + z-index: 2; +} + +.app-body { + flex: 1 1 auto; + position: relative; +} + +.app-container > .container:first-child { + padding: 2rem; +} + +@media (min-width: 960px) { + main > .container:first-child { + padding-top: 3rem; + padding-bottom: 3rem; + } +} + +.app-footer { + flex: 0 0 auto; +} + +.container { + max-width: 1224px; // should match auth-web, etc +} + +// Page Layout +.view-container { + display: flex; + flex-flow: column nowrap; + padding-top: 2.5rem; + padding-bottom: 2.5rem; + + article { + flex: 1 1 auto; + + section { + margin-top: 2.25rem; + } + } +} + +// Form Layout +.form__row + .form__row { + margin-top: 0.25rem; +} + +.form__btns { + display: flex; + + .v-btn { + margin: 0; + + + .v-btn { + margin-left: 0.5rem; + } + + &.form-primary-btn { + margin-left: auto; + } + } +} diff --git a/bcrs-ui/src/assets/styles/overrides.scss b/bcrs-ui/src/assets/styles/overrides.scss new file mode 100644 index 0000000..090a011 --- /dev/null +++ b/bcrs-ui/src/assets/styles/overrides.scss @@ -0,0 +1,64 @@ +// Vuetify Overrides +@import "theme.scss"; + +// Buttons +.v-btn { + min-width: 0; + text-transform: none; + font-weight: 500; + letter-spacing: normal; + + .v-icon + span, + span + .v-icon { + margin-left: 0.25rem; + } +} + +// Text Inputs +.v-text-field--box .v-label--active, +.v-text-field--full-width .v-label--active, +.v-text-field--outline .v-label--active { + transform: translateY(-8px) scale(0.75); +} + +// Dialogs +.v-dialog { + margin: 2rem; + + .v-card { + .v-card__title { + padding: 1.25rem 1.5rem; + color: $BCgovFontColorInverted; + background: $BCgovBlue5; + font-size: 1.5em; + font-weight: 400; + } + + .v-card__text { + padding: 1.5rem !important; + font-weight: 300; + } + + .v-card__actions { + padding: 1rem; + } + } +} + +// Expansion Panels +.v-expansion-panel:before { + box-shadow: none; +} + +.v-expansion-panel.align-items-top { + .v-expansion-panel-header__icon { + align-self: flex-start; + margin-top: 0; + } +} + +.v-expansion-panel-header.no-dropdown { + .v-expansion-panel-header__icon{ + display: none; + } +} diff --git a/bcrs-ui/src/assets/styles/theme.scss b/bcrs-ui/src/assets/styles/theme.scss new file mode 100644 index 0000000..90758f5 --- /dev/null +++ b/bcrs-ui/src/assets/styles/theme.scss @@ -0,0 +1,56 @@ +// Gray Palette +$gray0: #f8f9fa; +$gray1: #f1f3f5; +$gray2: #e9ecef; +$gray3: #dee2e6; +$gray4: #ced4da; +$gray5: #adb5bd; +$gray6: #868e96; +$gray7: #495057; +$gray8: #343a40; +$gray9: #212529; + +// BC GOV BLUE PALETTE +$BCgovBlue0: #e0e7ed; +$BCgovBlue1: #b3c2d1; +$BCgovBlue2: #8099b3; +$BCgovBlue3: #4d7094; +$BCgovBlue4: #26527d; +$BCgovBlue5: #003366; // PRIMARY BLUE +$BCgovBlue6: #002e5e; +$BCgovBlue7: #002753; +$BCgovBlue8: #002049; +$BCgovBlue9: #001438; + +// Accent Blue +$BCgovABlue1: #6e93ff; +$BCgovABlue2: #3b6cff; +$BCgovABlue3: #0846ff; +$BCgovABlue4: #003bee; + +// BC GOV GOLD PALETTE +$BCgovGold0: #fff7e3; +$BCgovGold1: #feeaba; +$BCgovGold2: #fedd8c; +$BCgovGold3: #fdcf5e; +$BCgovGold4: #fcc43c; +$BCgovGold5: #fcba19; // PRIMARY GOLD +$BCgovGold6: #fcb316; +$BCgovGold7: #fbab12; +$BCgovGold8: #fba30e; +$BCgovGold9: #fa9408; + +// Accent Gold +$BCgovAGold1: #ffffff; +$BCgovAGold2: #fff8ef; +$BCgovAGold3: #ffe0bc; +$BCgovAGold4: #ffd4a2; + +// Error Colors +$BCgovInputError: #ff5252; + +// Form Inputs +$BCgovInputBG: #ffffff; + +// Font Colors +$BCgovFontColorInverted: #ffffff; diff --git a/bcrs-ui/src/components/Home/HelloWorld.vue b/bcrs-ui/src/components/Home/HelloWorld.vue new file mode 100644 index 0000000..750f901 --- /dev/null +++ b/bcrs-ui/src/components/Home/HelloWorld.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/bcrs-ui/src/components/Home/index.ts b/bcrs-ui/src/components/Home/index.ts new file mode 100644 index 0000000..a877073 --- /dev/null +++ b/bcrs-ui/src/components/Home/index.ts @@ -0,0 +1,5 @@ +import HelloWorld from '@/components/Home/HelloWorld.vue' + +export { + HelloWorld +} diff --git a/bcrs-ui/src/components/common/ResourceExample.vue b/bcrs-ui/src/components/common/ResourceExample.vue new file mode 100644 index 0000000..778d7e0 --- /dev/null +++ b/bcrs-ui/src/components/common/ResourceExample.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/bcrs-ui/src/components/common/index.ts b/bcrs-ui/src/components/common/index.ts new file mode 100644 index 0000000..feabc0e --- /dev/null +++ b/bcrs-ui/src/components/common/index.ts @@ -0,0 +1,5 @@ +import ResourceExample from './ResourceExample.vue' + +export { + ResourceExample +} diff --git a/bcrs-ui/src/constants/index.ts b/bcrs-ui/src/constants/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/bcrs-ui/src/enums/index.ts b/bcrs-ui/src/enums/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/bcrs-ui/src/interfaces/action-interface.ts b/bcrs-ui/src/interfaces/action-interface.ts new file mode 100644 index 0000000..b00ac9b --- /dev/null +++ b/bcrs-ui/src/interfaces/action-interface.ts @@ -0,0 +1,10 @@ +import { ActionContext } from 'vuex' + +// Interface to define a Vuex Action +export interface ActionIF { + (x: ActionContext, y: any): void +} + +export interface ActionBindingIF { + (x: any): void +} diff --git a/bcrs-ui/src/interfaces/index.ts b/bcrs-ui/src/interfaces/index.ts new file mode 100644 index 0000000..9c6b44d --- /dev/null +++ b/bcrs-ui/src/interfaces/index.ts @@ -0,0 +1,3 @@ +export * from './resource-interface' +export * from './state-interface' +export * from './action-interface' diff --git a/bcrs-ui/src/interfaces/resource-interface.ts b/bcrs-ui/src/interfaces/resource-interface.ts new file mode 100644 index 0000000..4d26818 --- /dev/null +++ b/bcrs-ui/src/interfaces/resource-interface.ts @@ -0,0 +1,6 @@ +// Interface to define the resource model example +export interface ResourceExampleIF { + id: number, + displayName: string, + message: string +} diff --git a/bcrs-ui/src/interfaces/state-interface.ts b/bcrs-ui/src/interfaces/state-interface.ts new file mode 100644 index 0000000..133e478 --- /dev/null +++ b/bcrs-ui/src/interfaces/state-interface.ts @@ -0,0 +1,4 @@ +// Interface to define the state model example +export interface StateModelIF { + stateText: string | undefined +} diff --git a/bcrs-ui/src/main.ts b/bcrs-ui/src/main.ts new file mode 100644 index 0000000..3023046 --- /dev/null +++ b/bcrs-ui/src/main.ts @@ -0,0 +1,75 @@ +import 'core-js/stable' // to polyfill ECMAScript features +import 'regenerator-runtime/runtime' // to use transpiled generator functions + +// Vue Libraries +import Vue from 'vue' +import Vuetify from 'vuetify' +import router from './router' +import { store } from '@/store' + +// Styles +import '@/assets/styles/base.scss' +import '@/assets/styles/layout.scss' +import '@/assets/styles/overrides.scss' +import '@mdi/font/css/materialdesignicons.min.css' // ensure you are using css-loader + +// Base App +import App from './App.vue' + +// Helpers +import { fetchConfig, haveKcTokens } from '@/utils' +import TokenServices from 'sbc-common-components/src/services/token.services' + +// get rid of "You are running Vue in development mode" console message +Vue.config.productionTip = false + +Vue.use(Vuetify) + +const vuetify = new Vuetify({ iconfont: 'mdi' }) + +// ********************** THIS IS FOR TESTING & DEVELOPMENT ONLY *************************************** +// The following information allows the front to bypass authentication when developing locally +// whilst still being able to consume the entities and relationships apis. + +// eslint-disable-next-line +sessionStorage.setItem('KEYCLOAK_TOKEN', '...') +// eslint-disable-next-line +sessionStorage.setItem('KEYCLOAK_REFRESH_TOKEN', '...') +// eslint-disable-next-line +sessionStorage.setItem('KEYCLOAK_ID_TOKEN', '...') +sessionStorage.setItem('BUSINESS_IDENTIFIER', 'CP...') +sessionStorage.setItem('USER_FULL_NAME', 'Firstname Lastname') +// *************************************************************************************************** + +/** + * Fetch config from server, then load Vue + */ +fetchConfig() + .then(() => { + // ensure we have the necessary Keycloak tokens + if (!haveKcTokens()) { + console.info('Redirecting to Auth URL...') + const authUrl: string | null = sessionStorage.getItem('AUTH_URL') + // assume Auth URL is always reachable + authUrl && window.location.assign(authUrl) + return // do not execute remaining code + } + + // start token service to refresh KC token periodically + console.info('Starting token refresh service...') + const tokenServices = new TokenServices() + tokenServices.initUsingUrl(sessionStorage.getItem('KEYCLOAK_CONFIG_URL') || '') + .then(() => tokenServices.scheduleRefreshTimer()) + .catch((err: string) => console.error(err)) + + new Vue({ + vuetify, + router, + store, + render: h => h(App) + }).$mount('#app') + }) + .catch((error: string) => { + console.error('error fetching config -', error) + alert('Fatal error loading app') + }) diff --git a/bcrs-ui/src/mixins/common-mixin.ts b/bcrs-ui/src/mixins/common-mixin.ts new file mode 100644 index 0000000..54893b0 --- /dev/null +++ b/bcrs-ui/src/mixins/common-mixin.ts @@ -0,0 +1,16 @@ +import { Component, Vue } from 'vue-property-decorator' + +/** + * Mixin that provides some useful common utilities. + */ +@Component +export default class CommonMixin extends Vue { + /** + * This is an example mixin that will return a msg. + * + * @param msg The msg to return. + */ + sendMsg (msg: string): string { + return msg ? `${msg} - msg created by sendMsg mixin` : 'Error, no Message' + } +} diff --git a/bcrs-ui/src/mixins/index.ts b/bcrs-ui/src/mixins/index.ts new file mode 100644 index 0000000..0978fd7 --- /dev/null +++ b/bcrs-ui/src/mixins/index.ts @@ -0,0 +1,7 @@ +import CommonMixin from './common-mixin' +import ResourceLookupMixin from '@/mixins/resource-lookup-mixin' + +export { + CommonMixin, + ResourceLookupMixin +} diff --git a/bcrs-ui/src/mixins/resource-lookup-mixin.ts b/bcrs-ui/src/mixins/resource-lookup-mixin.ts new file mode 100644 index 0000000..ed57f3b --- /dev/null +++ b/bcrs-ui/src/mixins/resource-lookup-mixin.ts @@ -0,0 +1,34 @@ +// Libraries +import { Component, Vue } from 'vue-property-decorator' +import { State } from 'vuex-class' + +// Interfaces +import { ResourceExampleIF } from '@/interfaces' + +/** + * Mixin for components to retrieve text/settings from json resource. + */ +@Component +export default class ResourceLookupMixin extends Vue { + @State resourceModel!: Array + + /** + * Method to return the specified message + * + * @param id ID a number indicating the id of the resource to look up. + */ + getName (id: number): string { + const user: ResourceExampleIF | undefined = this.resourceModel && this.resourceModel.find(user => user.id === id) + return user ? user.displayName : '' + } + + /** + * Method to return the specified display Name + * + * @param id ID a number indicating the id of the resource to look up. + */ + getMessage (id: number): string { + const user: ResourceExampleIF | undefined = this.resourceModel && this.resourceModel.find(user => user.id === id) + return user ? user.message : '' + } +} diff --git a/bcrs-ui/src/resources/externalResourceExample.ts b/bcrs-ui/src/resources/externalResourceExample.ts new file mode 100644 index 0000000..fb37826 --- /dev/null +++ b/bcrs-ui/src/resources/externalResourceExample.ts @@ -0,0 +1,12 @@ +export const ExternalResource = [ + { + id: 1, + displayName: 'Cameron', + message: 'Hola, Como Estas?' + }, + { + id: 2, + displayName: 'Bob', + message: 'Muy bien, gracias. Y tu?' + } +] diff --git a/bcrs-ui/src/resources/index.ts b/bcrs-ui/src/resources/index.ts new file mode 100644 index 0000000..7f8f2f6 --- /dev/null +++ b/bcrs-ui/src/resources/index.ts @@ -0,0 +1 @@ +export * from './externalResourceExample' diff --git a/bcrs-ui/src/router/index.ts b/bcrs-ui/src/router/index.ts new file mode 100644 index 0000000..db2e766 --- /dev/null +++ b/bcrs-ui/src/router/index.ts @@ -0,0 +1,17 @@ +import Vue from 'vue' +import VueRouter from 'vue-router' +import { routes } from './routes' + +Vue.use(VueRouter) + +const router = new VueRouter({ + mode: 'history', + base: process.env.BASE_URL, + routes, + scrollBehavior (to, from, savedPosition) { + // see https://router.vuejs.org/guide/advanced/scroll-behavior.html + return { x: 0, y: 0 } + } +}) + +export default router diff --git a/bcrs-ui/src/router/routes.ts b/bcrs-ui/src/router/routes.ts new file mode 100644 index 0000000..10c7cbe --- /dev/null +++ b/bcrs-ui/src/router/routes.ts @@ -0,0 +1,35 @@ +import Home from '@/views/Home.vue' +import MixinExample from '@/views/MixinExample.vue' +import StateExample from '@/views/StateExample.vue' + +export const routes = [ + { + path: '/', + name: 'home', + component: Home, + meta: { + requiresAuth: false + } + }, + { + path: '/MixinExample', + name: 'mixinExample', + component: MixinExample, + meta: { + requiresAuth: false + } + }, + { + path: '/StateExample', + name: 'stateExample', + component: StateExample, + meta: { + requiresAuth: false + } + }, + { + // default/fallback route + path: '*', + redirect: '/' + } +] diff --git a/bcrs-ui/src/shims-tsx.d.ts b/bcrs-ui/src/shims-tsx.d.ts new file mode 100644 index 0000000..a175b0d --- /dev/null +++ b/bcrs-ui/src/shims-tsx.d.ts @@ -0,0 +1,13 @@ +import Vue, { VNode } from 'vue' + +declare global { + namespace JSX { + // tslint:disable no-empty-interface + interface Element extends VNode {} + // tslint:disable no-empty-interface + interface ElementClass extends Vue {} + interface IntrinsicElements { + [elem: string]: any; + } + } +} diff --git a/bcrs-ui/src/shims-vue.d.ts b/bcrs-ui/src/shims-vue.d.ts new file mode 100644 index 0000000..d9f24fa --- /dev/null +++ b/bcrs-ui/src/shims-vue.d.ts @@ -0,0 +1,4 @@ +declare module '*.vue' { + import Vue from 'vue' + export default Vue +} diff --git a/bcrs-ui/src/store/actions/actions-model.ts b/bcrs-ui/src/store/actions/actions-model.ts new file mode 100644 index 0000000..f36e73e --- /dev/null +++ b/bcrs-ui/src/store/actions/actions-model.ts @@ -0,0 +1,9 @@ +import { ActionIF } from '@/interfaces/action-interface' + +export const setName: ActionIF = ({ commit }, name): void => { + commit('mutateName', name) +} + +export const setResource: ActionIF = ({ commit }, resource): void => { + commit('mutateResource', resource) +} diff --git a/bcrs-ui/src/store/actions/index.ts b/bcrs-ui/src/store/actions/index.ts new file mode 100644 index 0000000..8922d30 --- /dev/null +++ b/bcrs-ui/src/store/actions/index.ts @@ -0,0 +1 @@ +export * from './actions-model' diff --git a/bcrs-ui/src/store/getters/index.ts b/bcrs-ui/src/store/getters/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/bcrs-ui/src/store/index.ts b/bcrs-ui/src/store/index.ts new file mode 100644 index 0000000..8823520 --- /dev/null +++ b/bcrs-ui/src/store/index.ts @@ -0,0 +1,29 @@ +// Libraries +import Vue from 'vue' +import Vuex, { Store } from 'vuex' + +// State +import { stateModel, resourceModel } from './state' + +// Actions +import { setName, setResource } from './actions' + +// Mutations +import { mutateName, mutateResource } from '@/store/mutations' + +Vue.use(Vuex) + +export const store: Store = new Vuex.Store({ + state: { + stateModel, + resourceModel + }, + mutations: { + mutateName, + mutateResource + }, + actions: { + setName, + setResource + } +}) diff --git a/bcrs-ui/src/store/mutations/index.ts b/bcrs-ui/src/store/mutations/index.ts new file mode 100644 index 0000000..ea0e9ac --- /dev/null +++ b/bcrs-ui/src/store/mutations/index.ts @@ -0,0 +1 @@ +export * from './mutations-model' diff --git a/bcrs-ui/src/store/mutations/mutations-model.ts b/bcrs-ui/src/store/mutations/mutations-model.ts new file mode 100644 index 0000000..d7163aa --- /dev/null +++ b/bcrs-ui/src/store/mutations/mutations-model.ts @@ -0,0 +1,7 @@ +export const mutateName = (state: any, name: string) => { + state.stateModel.stateText = name +} + +export const mutateResource = (state: any, resource: object) => { + state.resourceModel = resource +} diff --git a/bcrs-ui/src/store/state/index.ts b/bcrs-ui/src/store/state/index.ts new file mode 100644 index 0000000..61bfb15 --- /dev/null +++ b/bcrs-ui/src/store/state/index.ts @@ -0,0 +1,2 @@ +export * from './state-model' +export * from './resource-model' diff --git a/bcrs-ui/src/store/state/resource-model.ts b/bcrs-ui/src/store/state/resource-model.ts new file mode 100644 index 0000000..e18607d --- /dev/null +++ b/bcrs-ui/src/store/state/resource-model.ts @@ -0,0 +1,14 @@ +import { ResourceExampleIF } from '@/interfaces' + +export const resourceModel: Array = [ + { + id: 1, + displayName: '', + message: 'If you are reading this... my ACTIONS didnt work :(' + }, + { + id: 2, + displayName: '', + message: 'You should never see this text!' + } +] diff --git a/bcrs-ui/src/store/state/state-model.ts b/bcrs-ui/src/store/state/state-model.ts new file mode 100644 index 0000000..b608003 --- /dev/null +++ b/bcrs-ui/src/store/state/state-model.ts @@ -0,0 +1,5 @@ +import { StateModelIF } from '@/interfaces' + +export const stateModel: StateModelIF = { + stateText: 'Base State Text' +} diff --git a/bcrs-ui/src/utils/axios-auth.ts b/bcrs-ui/src/utils/axios-auth.ts new file mode 100644 index 0000000..32edea7 --- /dev/null +++ b/bcrs-ui/src/utils/axios-auth.ts @@ -0,0 +1,18 @@ +import axios from 'axios' + +const instance = axios.create() + +instance.interceptors.request.use( + config => { + config.headers.common['Authorization'] = `Bearer ${sessionStorage.getItem('KEYCLOAK_TOKEN')}` + return config + }, + error => Promise.reject(error) +) + +instance.interceptors.response.use( + response => response, + error => Promise.reject(error) +) + +export default instance diff --git a/bcrs-ui/src/utils/config-helper.ts b/bcrs-ui/src/utils/config-helper.ts new file mode 100644 index 0000000..5ed1772 --- /dev/null +++ b/bcrs-ui/src/utils/config-helper.ts @@ -0,0 +1,63 @@ +import axios from '@/utils/axios-auth' + +/** + * fetch config from environment and API + * + * @return A Promise to get & set session storage URLS with appropriate paths + */ +export const fetchConfig = (): Promise => { + const origin: string = window.location.origin + const vueAppPath: string = process.env.VUE_APP_PATH + const vueAppAuthPath:string = process.env.VUE_APP_AUTH_PATH + + if (!vueAppPath || !vueAppAuthPath) { + throw new Error('failed to get env variables') + } + + const baseUrl: string = `${origin}/${vueAppPath}/` + sessionStorage.setItem('BASE_URL', baseUrl) + console.log('Set Base URL to: ' + baseUrl) + + const authUrl: string = `${origin}/${vueAppAuthPath}/` + sessionStorage.setItem('AUTH_URL', authUrl) + console.log('Set Auth URL to: ' + authUrl) + + const url = `${origin}/${vueAppPath}/config/configuration.json` + console.log(url) + const headers = { + 'Accept': 'application/json', + 'ResponseType': 'application/json', + 'Cache-Control': 'no-cache' + } + + return axios + .get(url, { headers }) + .then((response: any) => { + const apiUrl: string = response.data['API_URL'] + axios.defaults.baseURL = apiUrl + console.log('Set Legal API URL to: ' + apiUrl) + + const authApiUrl: string = response.data['AUTH_API_URL'] + sessionStorage.setItem('AUTH_API_URL', authApiUrl) + console.log('Set Auth API URL to: ' + authApiUrl) + + const payApiUrl: string = response.data['PAY_API_URL'] + sessionStorage.setItem('PAY_API_URL', payApiUrl) + console.log('Set Pay API URL to: ' + payApiUrl) + + const keycloakConfigUrl = response.data['KEYCLOAK_CONFIG_URL'] + sessionStorage.setItem('KEYCLOAK_CONFIG_URL', keycloakConfigUrl) + console.info('Set KeyCloak config URL to: ' + keycloakConfigUrl) + }) +} + +/** + * Validate the KeyCloak tokens + * + * @return A boolean indicating if all keycloak variations exist + */ +export const haveKcTokens = (): boolean => { + return Boolean(sessionStorage.getItem('KEYCLOAK_TOKEN') && + sessionStorage.getItem('KEYCLOAK_REFRESH_TOKEN') && + sessionStorage.getItem('KEYCLOAK_ID_TOKEN')) +} diff --git a/bcrs-ui/src/utils/index.ts b/bcrs-ui/src/utils/index.ts new file mode 100644 index 0000000..17e5c91 --- /dev/null +++ b/bcrs-ui/src/utils/index.ts @@ -0,0 +1 @@ +export * from './config-helper' diff --git a/bcrs-ui/src/views/Home.vue b/bcrs-ui/src/views/Home.vue new file mode 100644 index 0000000..9997b93 --- /dev/null +++ b/bcrs-ui/src/views/Home.vue @@ -0,0 +1,21 @@ + + + diff --git a/bcrs-ui/src/views/MixinExample.vue b/bcrs-ui/src/views/MixinExample.vue new file mode 100644 index 0000000..c3f281a --- /dev/null +++ b/bcrs-ui/src/views/MixinExample.vue @@ -0,0 +1,13 @@ + + diff --git a/bcrs-ui/src/views/StateExample.vue b/bcrs-ui/src/views/StateExample.vue new file mode 100644 index 0000000..3da8a7d --- /dev/null +++ b/bcrs-ui/src/views/StateExample.vue @@ -0,0 +1,59 @@ + + diff --git a/bcrs-ui/tests/unit/example.spec.ts b/bcrs-ui/tests/unit/example.spec.ts new file mode 100644 index 0000000..6d12291 --- /dev/null +++ b/bcrs-ui/tests/unit/example.spec.ts @@ -0,0 +1,12 @@ +import { shallowMount } from '@vue/test-utils' +import { HelloWorld } from '@/components/Home' + +describe('HelloWorld.vue', () => { + it('renders props.msg when passed', () => { + const msg = 'new message' + const wrapper = shallowMount(HelloWorld, { + propsData: { msg } + }) + expect(wrapper.text()).toMatch(msg) + }) +}) diff --git a/bcrs-ui/tests/unit/stateExample.spec.ts b/bcrs-ui/tests/unit/stateExample.spec.ts new file mode 100644 index 0000000..a792998 --- /dev/null +++ b/bcrs-ui/tests/unit/stateExample.spec.ts @@ -0,0 +1,35 @@ +// Libraries +import Vue from 'vue' +import Vuetify from 'vuetify' +import { store } from '@/store' +import { shallowMount } from '@vue/test-utils' + +// Components +import StateExample from '@/views/StateExample.vue' +import { ResourceExample } from '@/components/common' + +Vue.use(Vuetify) +let vuetify = new Vuetify({}) + +describe('HelloWorld.vue', () => { + let wrapper: any + + beforeEach(() => { + // create wrapper for Dashboard + // this stubs out the sub-component + wrapper = shallowMount(StateExample, { store, vuetify }) + }) + + afterEach(() => { + wrapper.destroy() + }) + + it('renders the sub-components properly', () => { + expect(wrapper.find(ResourceExample).exists()).toBe(true) + }) + + it('displays the appropriate welcome message', () => { + expect(wrapper.vm.$el.querySelector('.stateExample').textContent) + .toContain('Congratulations... it worked!') + }) +}) diff --git a/bcrs-ui/tsconfig.json b/bcrs-ui/tsconfig.json new file mode 100644 index 0000000..89c1ab0 --- /dev/null +++ b/bcrs-ui/tsconfig.json @@ -0,0 +1,42 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "strict": true, + "strictBindCallApply": true, + "jsx": "preserve", + "importHelpers": true, + "moduleResolution": "node", + "experimentalDecorators": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "sourceMap": true, + "baseUrl": ".", + "types": [ + "webpack-env", + "jest", + "vuetify" + ], + "paths": { + "@/*": [ + "src/*" + ] + }, + "lib": [ + "esnext", + "dom", + "dom.iterable", + "scripthost" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.vue", + "tests/**/*.ts", + "tests/**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/bcrs-ui/vue.config.js b/bcrs-ui/vue.config.js new file mode 100644 index 0000000..295307e --- /dev/null +++ b/bcrs-ui/vue.config.js @@ -0,0 +1,21 @@ +module.exports = { + configureWebpack: { + devtool: 'source-map' + }, + transpileDependencies: [ + 'vue-plugin-helper-decorator', + 'vuetify' + ], + publicPath: `/${process.env.VUE_APP_PATH}`, + devServer: { + proxy: { + // this is needed to prevent a CORS error when running locally + '/local-keycloak-config-url/*': { + target: 'https://dev.bcregistry.ca/cooperatives/auth/config/kc/', + pathRewrite: { + '/local-keycloak-config-url': '' + } + } + } + } +}