Renamed everything from MultiShop to Props.
3
Props/client/.browserslistrc
Normal file
@@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
6
Props/client/.editorconfig
Normal file
@@ -0,0 +1,6 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
quote_type = double
|
33
Props/client/.eslintrc.js
Normal file
@@ -0,0 +1,33 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/vue3-essential',
|
||||
'@vue/standard'
|
||||
],
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'quotes': ['error', 'double', 'avoid-escape'],
|
||||
'semi': ['error', 'always'],
|
||||
'indent': ['error', 4],
|
||||
'comma-dangle': ['error', 'only-multiline'],
|
||||
'space-before-function-paren': ['error', 'never']
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'**/__tests__/*.{j,t}s?(x)',
|
||||
'**/tests/unit/**/*.spec.{j,t}s?(x)'
|
||||
],
|
||||
env: {
|
||||
mocha: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
25
Props/client/.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
/tests/e2e/logs/
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
34
Props/client/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# props
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run your unit tests
|
||||
```
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### Run your end-to-end tests
|
||||
```
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
Props/client/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
5
Props/client/jsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
]
|
||||
}
|
18602
Props/client/package-lock.json
generated
Normal file
48
Props/client/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "props",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"test:e2e": "vue-cli-service test:e2e",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"bootstrap": "^5.0.2",
|
||||
"bootstrap-icons": "^1.5.0",
|
||||
"core-js": "^3.6.5",
|
||||
"oidc-client": "^1.11.5",
|
||||
"register-service-worker": "^1.7.1",
|
||||
"vue": "^3.0.0",
|
||||
"vue-router": "^4.0.0-0",
|
||||
"vuex": "^4.0.0-0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-e2e-webdriverio": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-pwa": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-plugin-unit-mocha": "~4.5.0",
|
||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/compiler-sfc": "^3.0.0",
|
||||
"@vue/eslint-config-standard": "^5.1.2",
|
||||
"@vue/test-utils": "^2.0.0-0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"chai": "^4.1.2",
|
||||
"chromedriver": "91",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^7.0.0",
|
||||
"sass": "^1.35.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"wdio-chromedriver-service": "^6.0.3"
|
||||
}
|
||||
}
|
13
Props/client/public/authentication/callback-handler.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { UserManager, WebStorageStateStore } from "oidc-client";
|
||||
|
||||
const userManager = new UserManager({
|
||||
authority: window.location.origin,
|
||||
client_id: "Props",
|
||||
redirect_uri: window.location.origin + "/authentication/login-callback",
|
||||
post_logout_redirect_uri: window.location.origin + "/authentication/logout-callback",
|
||||
response_type: "code",
|
||||
scope: "openid profile",
|
||||
userStore: new WebStorageStateStore({ store: window.localStorage }),
|
||||
});
|
||||
|
||||
userManager.signinSilentCallback();
|
@@ -0,0 +1,16 @@
|
||||
<!-- Completely separate static html should improve silent login performance as we don't need to load entire SPA. -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>authentication</title>
|
||||
<script type="module" src="callback-handler.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
Silently authenticating user. If you are seeing this page, you probably want to <a href="/">go back to the app</a>.
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
BIN
Props/client/public/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
Props/client/public/img/icons/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
Props/client/public/img/icons/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 22 KiB |
BIN
Props/client/public/img/icons/apple-touch-icon-120x120.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
Props/client/public/img/icons/apple-touch-icon-152x152.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
Props/client/public/img/icons/apple-touch-icon-180x180.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
Props/client/public/img/icons/apple-touch-icon-60x60.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
Props/client/public/img/icons/apple-touch-icon-76x76.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
Props/client/public/img/icons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
Props/client/public/img/icons/favicon-16x16.png
Normal file
After Width: | Height: | Size: 799 B |
BIN
Props/client/public/img/icons/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
Props/client/public/img/icons/msapplication-icon-144x144.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
Props/client/public/img/icons/mstile-150x150.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
3
Props/client/public/img/icons/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.00251 14.9297L0 1.07422H6.14651L8.00251 4.27503L9.84583 1.07422H16L8.00251 14.9297Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 215 B |
17
Props/client/public/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
2
Props/client/public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
76
Props/client/src/App.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div id="app-content" class="theme-light">
|
||||
<nav class="navbar" id="nav">
|
||||
<div class="container-fluid">
|
||||
<router-link class="navbar-brand" to="/">Props</router-link>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarContent"
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<router-link
|
||||
active-class="active"
|
||||
class="nav-link"
|
||||
to="/"
|
||||
>Home</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<router-link
|
||||
active-class="active"
|
||||
class="nav-link"
|
||||
to="/about"
|
||||
>About</router-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<ProfileDisplay>
|
||||
</ProfileDisplay>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<ProfileSignUp>
|
||||
</ProfileSignUp>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<ProfileLogIn>
|
||||
</ProfileLogIn>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<ProfileLogOut>
|
||||
</ProfileLogOut>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "bootstrap/js/dist/collapse";
|
||||
import ProfileDisplay from "./components/ProfileDisplay.vue";
|
||||
import ProfileLogIn from "./components/ProfileLogIn.vue";
|
||||
import ProfileLogOut from "./components/ProfileLogOut.vue";
|
||||
import ProfileSignUp from "./components/ProfileSignUp.vue";
|
||||
export default {
|
||||
components: {
|
||||
ProfileDisplay,
|
||||
ProfileLogIn,
|
||||
ProfileSignUp,
|
||||
ProfileLogOut
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch("updatePublicApiSettings");
|
||||
this.$store.dispatch("loadUser");
|
||||
}
|
||||
};
|
||||
</script>
|
BIN
Props/client/src/assets/images/logo.png
Normal file
After Width: | Height: | Size: 21 KiB |
249
Props/client/src/assets/images/logo.svg
Normal file
@@ -0,0 +1,249 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="130.02815mm"
|
||||
height="183.73235mm"
|
||||
viewBox="0 0 130.02815 183.73235"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:export-filename="C:\Users\yunya\Documents\Props\Props\client\src\assets\images\logo.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:snap-global="true"
|
||||
inkscape:zoom="1.0607991"
|
||||
inkscape:cx="181.46697"
|
||||
inkscape:cy="340.78083"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1027"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-40.717182,-65.583996)">
|
||||
<rect
|
||||
style="fill:#51b6bf;fill-opacity:1;stroke:none;stroke-width:0.394384"
|
||||
id="rect846"
|
||||
width="130.02815"
|
||||
height="180.11604"
|
||||
x="40.717182"
|
||||
y="69.200294"
|
||||
ry="7.2278728" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.355205"
|
||||
id="rect6139"
|
||||
width="114.73653"
|
||||
height="165.58026"
|
||||
x="48.362995"
|
||||
y="76.468185"
|
||||
ry="6.6445661" />
|
||||
<g
|
||||
id="g20855"
|
||||
transform="translate(0,-4.5861139)">
|
||||
<rect
|
||||
style="fill:none;fill-opacity:1;stroke:#3b3485;stroke-width:1.465;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect4822-1"
|
||||
width="8.1061144"
|
||||
height="8.1061144"
|
||||
x="56.92775"
|
||||
y="113.97274"
|
||||
ry="1.3718038" />
|
||||
<rect
|
||||
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:1.86051;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5136-7"
|
||||
width="42.169209"
|
||||
height="3.4690557"
|
||||
x="69.150803"
|
||||
y="116.29127"
|
||||
ry="1.7345278" />
|
||||
</g>
|
||||
<g
|
||||
id="g20859"
|
||||
transform="translate(0,-7.0555611)">
|
||||
<rect
|
||||
style="fill:none;fill-opacity:1;stroke:#3b3485;stroke-width:1.465;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect4822-14"
|
||||
width="8.1061144"
|
||||
height="8.1061144"
|
||||
x="56.92775"
|
||||
y="127.76641"
|
||||
ry="1.3718038" />
|
||||
<rect
|
||||
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:1.43107;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5136-7-2"
|
||||
width="21.986813"
|
||||
height="3.9363635"
|
||||
x="68.953468"
|
||||
y="129.85129"
|
||||
ry="1.9681817" />
|
||||
</g>
|
||||
<g
|
||||
id="g20845"
|
||||
transform="translate(0,-2.1166667)">
|
||||
<rect
|
||||
style="fill:none;fill-opacity:1;stroke:#3b3485;stroke-width:1.465;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect4822"
|
||||
width="8.1061144"
|
||||
height="8.1061144"
|
||||
x="56.92775"
|
||||
y="100.17907"
|
||||
ry="1.3718038" />
|
||||
<rect
|
||||
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:1.59414;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5136"
|
||||
width="28.571259"
|
||||
height="3.7589138"
|
||||
x="69.028404"
|
||||
y="102.35267"
|
||||
ry="1.8794569" />
|
||||
<rect
|
||||
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:1.50408;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5136-5"
|
||||
width="24.788029"
|
||||
height="3.8569105"
|
||||
x="102.89133"
|
||||
y="102.30367"
|
||||
ry="1.9284552" />
|
||||
</g>
|
||||
<g
|
||||
id="g20863"
|
||||
transform="translate(0,-9.5250005)">
|
||||
<rect
|
||||
style="fill:none;fill-opacity:1;stroke:#3b3485;stroke-width:1.465;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect4822-1-4"
|
||||
width="8.1061144"
|
||||
height="8.1061144"
|
||||
x="56.92775"
|
||||
y="141.56007"
|
||||
ry="1.3718038" />
|
||||
<rect
|
||||
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:1.57172;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5728"
|
||||
width="27.594185"
|
||||
height="3.7833092"
|
||||
x="69.018089"
|
||||
y="143.72148"
|
||||
ry="1.8916546" />
|
||||
</g>
|
||||
<rect
|
||||
style="fill:#202042;fill-opacity:1;stroke:none;stroke-width:0.23824"
|
||||
id="rect1392"
|
||||
width="53.152565"
|
||||
height="18.233921"
|
||||
x="-132.30754"
|
||||
y="-83.817917"
|
||||
ry="3.4788733"
|
||||
transform="scale(-1)" />
|
||||
<rect
|
||||
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:2.22336;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect6412"
|
||||
width="73.657707"
|
||||
height="2.8362327"
|
||||
x="56.336582"
|
||||
y="201.33577"
|
||||
ry="1.4181163" />
|
||||
<rect
|
||||
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:2.23876;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect6414"
|
||||
width="69.712616"
|
||||
height="3.0383809"
|
||||
x="56.344284"
|
||||
y="209.24466"
|
||||
ry="1.5191904" />
|
||||
<rect
|
||||
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:2.42357;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect6416"
|
||||
width="86.988022"
|
||||
height="2.8535743"
|
||||
x="56.436687"
|
||||
y="217.23825"
|
||||
ry="1.4267871" />
|
||||
<rect
|
||||
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:2.38882;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect6418"
|
||||
width="83.495445"
|
||||
height="2.8883159"
|
||||
x="56.419315"
|
||||
y="225.12207"
|
||||
ry="1.444158" />
|
||||
<rect
|
||||
style="fill:#2f3898;fill-opacity:1;stroke:none;stroke-width:3.0459;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect6295-8"
|
||||
width="57.490276"
|
||||
height="6.8199105"
|
||||
x="76.986122"
|
||||
y="87.608482"
|
||||
ry="3.4099553" />
|
||||
<rect
|
||||
style="fill:#2f3898;fill-opacity:1;stroke:none;stroke-width:2.58155;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect6490"
|
||||
width="41.297459"
|
||||
height="6.8199105"
|
||||
x="85.082527"
|
||||
y="188.09093"
|
||||
ry="3.4099553" />
|
||||
<rect
|
||||
style="fill:#2f3898;fill-opacity:1;stroke:none;stroke-width:1.87563;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect6490-3"
|
||||
width="21.8001"
|
||||
height="6.8199105"
|
||||
x="94.831207"
|
||||
y="151.63762"
|
||||
ry="3.4099553" />
|
||||
<rect
|
||||
style="opacity:1;fill:#2bc8d7;fill-opacity:1;stroke:none;stroke-width:1.63579;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect6492"
|
||||
width="16.931116"
|
||||
height="13.580167"
|
||||
x="132.27409"
|
||||
y="201.23309"
|
||||
ry="2.6454868" />
|
||||
<rect
|
||||
style="opacity:1;fill:#dcf8f6;fill-opacity:1;stroke:none;stroke-width:1.465;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect6932"
|
||||
width="79.717339"
|
||||
height="19.929335"
|
||||
x="65.872589"
|
||||
y="161.02196"
|
||||
ry="5.6437054" />
|
||||
<path
|
||||
style="fill:none;stroke:#3d39a9;stroke-width:1.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 67.636283,179.27583 17.812943,-8.46556 24.779394,3.96823 34.12678,-11.19923"
|
||||
id="path7699"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#6f78b2;stroke-width:0.765;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 65.872624,169.39934 37.918646,0.17637 17.98931,5.20279 23.63302,-7.14281"
|
||||
id="path8022" />
|
||||
<rect
|
||||
style="opacity:1;fill:none;fill-opacity:1;stroke:#b8edf3;stroke-width:1.465;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect9952"
|
||||
width="79.717339"
|
||||
height="19.929335"
|
||||
x="65.872589"
|
||||
y="161.02196"
|
||||
ry="5.6437054" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.4 KiB |
9
Props/client/src/assets/scss/_app-layout.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
// Used to provide inial layout for application containing elements.
|
||||
html, body, #app, #app-content {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#app-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
17
Props/client/src/assets/scss/_themer.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
@use "sass:map";
|
||||
@use "variables";
|
||||
|
||||
// Applied Dmitry Borody's method for theming with modifications. - https://medium.com/@dmitriy.borodiy/easy-color-theming-with-scss-bc38fd5734d1
|
||||
@mixin themed($themes: variables.$themes) {
|
||||
@each $theme, $vars in $themes {
|
||||
.theme-#{$theme} &, &.theme-#{$theme} {
|
||||
$theme-values: $vars !global;
|
||||
@content;
|
||||
$theme-values: null !global;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@function color-of($type) {
|
||||
@return map.get($theme-values, $type);
|
||||
}
|
3
Props/client/src/assets/scss/_variables.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
$themes: (
|
||||
"light": ("background": #F4F4F4, "navbar": #FFF7F7, "main": #BDF2D5, "sub": #F2FCFC, "bold": #1E56A0, "text": #1A1A1A),
|
||||
);
|
48
Props/client/src/assets/scss/main.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
@use "app-layout";
|
||||
@use "../../../node_modules/bootstrap/scss/bootstrap";
|
||||
|
||||
#app-content {
|
||||
@include themer.themed {
|
||||
background-color: themer.color-of("background");
|
||||
color: themer.color-of("text");
|
||||
}
|
||||
}
|
||||
|
||||
nav.navbar {
|
||||
@extend .navbar-expand-lg;
|
||||
@extend .sticky-top;
|
||||
@include themer.themed {
|
||||
@extend .navbar-light;
|
||||
background-color: themer.color-of("navbar");
|
||||
}
|
||||
}
|
||||
|
||||
.jumbotron {
|
||||
@extend .container-fluid;
|
||||
@extend .p-4;
|
||||
@include themer.themed {
|
||||
background-color: themer.color-of("main");
|
||||
}
|
||||
|
||||
&.sub {
|
||||
@include themer.themed {
|
||||
background-color: themer.color-of("sub");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 5em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
hr {
|
||||
@extend .my-3;
|
||||
@include themer.themed {
|
||||
color: themer.color-of("bold");
|
||||
}
|
||||
}
|
||||
|
19
Props/client/src/components/InfoCard.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><slot name="title">Title</slot></h5>
|
||||
<h6 class="card-subtitle mb-3 text-muted">
|
||||
<slot name="subtitle">Subtitle</slot>
|
||||
</h6>
|
||||
<p class="card-text">
|
||||
<slot name="text"> </slot>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "InfoCard"
|
||||
};
|
||||
</script>
|
53
Props/client/src/components/ProfileDisplay.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<a v-if="visible" :href="manageUrl" class="btn">
|
||||
<slot v-if="username" :displayName="username" name="username">
|
||||
{{ username }}
|
||||
</slot>
|
||||
<slot v-else-if="isProfileLoading" name="loading">
|
||||
<WaitCircle class="spinner-grow-sm"></WaitCircle>
|
||||
</slot>
|
||||
<slot v-else name="unauthenticated">
|
||||
<span>Not Logged In</span>
|
||||
</slot>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { identityPaths } from "../services/authentication";
|
||||
import WaitCircle from "./WaitCircle.vue";
|
||||
export default {
|
||||
components: {
|
||||
WaitCircle
|
||||
},
|
||||
name: "ProfileDisplay",
|
||||
props: {
|
||||
showUnauthenticated: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
manageUrl: identityPaths.Manage,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
username() {
|
||||
return this.$store.getters.username &&
|
||||
!this.$store.getters.isIdentityLoading
|
||||
? this.$store.getters.username
|
||||
: null;
|
||||
},
|
||||
visible() {
|
||||
return !this.showUnauthenticated
|
||||
? this.$store.getters.isAuthenticated ||
|
||||
this.$store.getters.isIdentityLoading
|
||||
: true;
|
||||
},
|
||||
isProfileLoading() {
|
||||
return this.$store.getters.isIdentityLoading;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
21
Props/client/src/components/ProfileLogIn.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<button v-if="visible" @click="onClick" type="button" class="btn">
|
||||
<slot>Log In</slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ProfileLogIn",
|
||||
computed: {
|
||||
visible() {
|
||||
return !this.$store.getters.isAuthenticated && !this.$store.getters.isIdentityLoading;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$store.dispatch("beginAuthentication");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
21
Props/client/src/components/ProfileLogOut.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<button v-if="visible" @click="onClick" type="button" class="btn">
|
||||
<slot>Log Out</slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ProfileLogOut",
|
||||
computed: {
|
||||
visible() {
|
||||
return this.$store.getters.isAuthenticated && !this.$store.getters.isIdentityLoading;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$store.dispatch("beginDeauthentication");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
21
Props/client/src/components/ProfileSignUp.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<button v-if="visible" @click="onClick" type="button" class="btn">
|
||||
<slot>Sign Up!</slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ProfileSignUp",
|
||||
computed: {
|
||||
visible() {
|
||||
return this.$store.state.identity.registrationEnabled && !this.$store.getters.isAuthenticated && !this.$store.getters.isIdentityLoading;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$store.dispatch("beginRegistration");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
39
Props/client/src/components/SearchBar.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<form autocomplete="off">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" :placeholder="placeholder" v-model="query" @keyup.enter="onEnter">
|
||||
<slot name="append"></slot>
|
||||
<button class="btn btn-outline-primary" type="button" @click="onClick">
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
placeholder: String,
|
||||
searchCallback: Function
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
query: "",
|
||||
disabled: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.disabled = true;
|
||||
this.searchCallback(this.query);
|
||||
this.disabled = false;
|
||||
},
|
||||
onEnter() {
|
||||
this.disabled = true;
|
||||
this.searchCallback(this.query);
|
||||
this.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
24
Props/client/src/components/WaitCircle.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mx-auto" id="spinner-container">
|
||||
<div class="spinner-grow" role="status" v-bind="$attrs">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inheritAttrs: false
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#spinner-container {
|
||||
width: fit-content;
|
||||
}
|
||||
</style>
|
15
Props/client/src/main.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import "./registerServiceWorker";
|
||||
import router from "./router";
|
||||
import store from "./store";
|
||||
|
||||
import "../node_modules/bootstrap-icons/font/bootstrap-icons.css";
|
||||
import "./assets/scss/main.scss"; // Main global scss file.
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(store);
|
||||
app.use(router);
|
||||
|
||||
app.mount("#app");
|
32
Props/client/src/registerServiceWorker.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { register } from "register-service-worker";
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||
ready() {
|
||||
console.log(
|
||||
"App is being served from cache by a service worker.\n" +
|
||||
"For more details, visit https://goo.gl/AFskqB"
|
||||
);
|
||||
},
|
||||
registered() {
|
||||
console.log("Service worker has been registered.");
|
||||
},
|
||||
cached() {
|
||||
console.log("Content has been cached for offline use.");
|
||||
},
|
||||
updatefound() {
|
||||
console.log("New content is downloading.");
|
||||
},
|
||||
updated() {
|
||||
console.log("New content is available; please refresh.");
|
||||
},
|
||||
offline() {
|
||||
console.log("No internet connection found. App is running in offline mode.");
|
||||
},
|
||||
error(error) {
|
||||
console.error("Error during service worker registration:", error);
|
||||
}
|
||||
});
|
||||
}
|
13
Props/client/src/router/guards.js
Normal file
@@ -0,0 +1,13 @@
|
||||
function authenticationGuard(app, to, from, next) {
|
||||
if (to.authenticationRequired) {
|
||||
if (app.$store.getters.identity.isAuthenticated) {
|
||||
next();
|
||||
} else {
|
||||
app.$store.dispatch("beginAuthentication");
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
export { authenticationGuard };
|
11
Props/client/src/router/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import { authenticationGuard } from "./guards";
|
||||
import { routes } from "./routes";
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => authenticationGuard(router, to, from, next));
|
||||
|
||||
export default router;
|
25
Props/client/src/router/routes.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import Home from "../views/Home.vue";
|
||||
import Authentication from "../views/Authentication.vue";
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
name: "Home",
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: "/about",
|
||||
name: "About",
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import(/* webpackChunkName: "about" */ "../views/About.vue")
|
||||
},
|
||||
{
|
||||
path: "/authentication/:action?",
|
||||
name: "Authentication",
|
||||
component: Authentication,
|
||||
props: true,
|
||||
}
|
||||
];
|
||||
|
||||
export { routes };
|
22
Props/client/src/services/authentication.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { UserManager, WebStorageStateStore } from "oidc-client";
|
||||
|
||||
const userManager = new UserManager({
|
||||
authority: window.location.origin,
|
||||
client_id: "Props",
|
||||
redirect_uri: window.location.origin + "/authentication/login-callback",
|
||||
post_logout_redirect_uri: window.location.origin + "/authentication/logout-callback",
|
||||
response_type: "code",
|
||||
scope: "openid profile",
|
||||
userStore: new WebStorageStateStore({ store: window.localStorage }),
|
||||
});
|
||||
|
||||
const identityPaths = {
|
||||
Manage: "Identity/Account/Manage",
|
||||
Register: "/Identity/Account/Register"
|
||||
};
|
||||
|
||||
const identityQueryParameters = {
|
||||
ReturnUrl: "returnUrl",
|
||||
};
|
||||
|
||||
export { userManager, identityPaths, identityQueryParameters };
|
23
Props/client/src/services/http.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import axios from "axios";
|
||||
|
||||
let currentAuthorizationInterceptorID = null;
|
||||
|
||||
const apiHttp = axios.create({
|
||||
baseURL: window.location.origin + "/api",
|
||||
timeout: 3000,
|
||||
});
|
||||
|
||||
function addBearerTokenInterceptor(token) {
|
||||
currentAuthorizationInterceptorID = apiHttp.interceptors.request.use((config) => {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
return config;
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
function removeBearerTokenInterceptor() {
|
||||
apiHttp.interceptors.request.eject(currentAuthorizationInterceptorID);
|
||||
}
|
||||
|
||||
export { apiHttp, addBearerTokenInterceptor, removeBearerTokenInterceptor };
|
31
Props/client/src/services/persistence.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const prefix = "Props";
|
||||
function put(key, value) {
|
||||
if (value == null) return false;
|
||||
try {
|
||||
localStorage.setItem(prefix + ":" + key, JSON.stringify(value));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function get(key) {
|
||||
if (!exists(key)) return null;
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(prefix + ":" + key));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function remove(key) {
|
||||
localStorage.removeItem(prefix + ":" + key);
|
||||
}
|
||||
|
||||
function exists(key) {
|
||||
return localStorage.getItem(prefix + ":" + key) != null;
|
||||
}
|
||||
|
||||
export { put, get, remove, exists };
|
132
Props/client/src/store/identity.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import router from "../router";
|
||||
import { identityPaths, identityQueryParameters, userManager } from "../services/authentication";
|
||||
import { addBearerTokenInterceptor, apiHttp, removeBearerTokenInterceptor } from "../services/http";
|
||||
import { get, put } from "../services/persistence";
|
||||
|
||||
const identity = {
|
||||
state: () => ({
|
||||
user: null,
|
||||
loadingLayers: 0,
|
||||
callbackPath: null,
|
||||
registrationEnabled: false,
|
||||
}),
|
||||
getters: {
|
||||
isAuthenticated(state) {
|
||||
return state.user ? !state.user.expired : false;
|
||||
},
|
||||
isIdentityLoading(state) {
|
||||
return state.loadingLayers > 0;
|
||||
},
|
||||
username(state) {
|
||||
return (state.user && state.user.profile.name) ? state.user.profile.name : null;
|
||||
},
|
||||
authCallbackLocation(state) {
|
||||
return state.callbackPath ? state.callbackPath : get("callbackPath");
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
login(state, { user }) {
|
||||
if (!user) return;
|
||||
state.user = user;
|
||||
addBearerTokenInterceptor(user.access_token);
|
||||
},
|
||||
logout(state) {
|
||||
state.user = null;
|
||||
removeBearerTokenInterceptor();
|
||||
},
|
||||
beginAuthenticating(state) {
|
||||
state.loading += 1;
|
||||
},
|
||||
endAuthenticating(state) {
|
||||
state.loading -= 1;
|
||||
},
|
||||
authCallbackLocation(state) {
|
||||
state.callbackPath = window.location.pathname;
|
||||
put("callbackPath", state.callbackPath);
|
||||
},
|
||||
enableIdentificationRegistration(state) {
|
||||
state.registrationEnabled = true;
|
||||
},
|
||||
disableIdentificationRegistration(state) {
|
||||
state.registrationEnabled = false;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async loadUser(context) {
|
||||
context.commit("beginAuthenticating");
|
||||
const user = await userManager.getUser();
|
||||
if (user != null) {
|
||||
context.commit("login", { user });
|
||||
}
|
||||
context.commit("endAuthenticating");
|
||||
},
|
||||
async attemptSilentAuthentication(context, options) {
|
||||
if (context.getters.isAuthenticated) return;
|
||||
context.commit("beginAuthenticating");
|
||||
context.dispatch("loadUser");
|
||||
if (!context.getters.isAuthenticated) {
|
||||
try {
|
||||
const user = await userManager.signinSilent({
|
||||
redirect_uri: window.location.origin + "/authentication/silent-login-callback.html"
|
||||
});
|
||||
|
||||
context.commit("login", { user });
|
||||
} catch { }
|
||||
context.commit("endAuthenticating");
|
||||
}
|
||||
|
||||
if (options && options.redirect) {
|
||||
router.replace(context.getters.authCallbackLocation);
|
||||
}
|
||||
},
|
||||
async beginAuthentication(context) {
|
||||
if (context.getters.isAuthenticated) return;
|
||||
context.commit("beginAuthenticating");
|
||||
context.dispatch("loadUser");
|
||||
if (!context.getters.isAuthenticated) {
|
||||
context.commit("authCallbackLocation");
|
||||
userManager.signinRedirect();
|
||||
}
|
||||
},
|
||||
async beginDeauthentication(context) {
|
||||
if (!context.getters.isAuthenticated) return;
|
||||
context.commit("beginAuthenticating");
|
||||
context.commit("authCallbackLocation");
|
||||
userManager.signoutRedirect();
|
||||
},
|
||||
async completeAuthentication(context, { user }) {
|
||||
if (!user) return;
|
||||
context.commit("login", { user });
|
||||
context.commit("endAuthenticating");
|
||||
router.replace(context.getters.authCallbackLocation);
|
||||
},
|
||||
async completeDeauthentication(context) {
|
||||
context.commit("logout");
|
||||
try {
|
||||
await userManager.removeUser();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
context.commit("endAuthenticating");
|
||||
router.replace(context.getters.authCallbackLocation);
|
||||
},
|
||||
beginRegistration(context) {
|
||||
context.commit("authCallbackLocation");
|
||||
window.location.replace(window.location.origin + identityPaths.Register + "?" + identityQueryParameters.ReturnUrl + "=" + "/authentication/silent-login");
|
||||
},
|
||||
async updatePublicApiSettings(context) {
|
||||
try {
|
||||
const settings = (await apiHttp.get("PublicApiSettings")).data;
|
||||
if (settings.RegistrationEnabled === "True") {
|
||||
context.commit("enableIdentificationRegistration");
|
||||
} else {
|
||||
context.commit("disableIdentificationRegistration");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export { identity };
|
16
Props/client/src/store/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createStore } from "vuex";
|
||||
import { identity } from "./identity";
|
||||
|
||||
export default createStore({
|
||||
state: {
|
||||
},
|
||||
getters: {
|
||||
},
|
||||
mutations: {
|
||||
},
|
||||
actions: {
|
||||
},
|
||||
modules: {
|
||||
identity: identity,
|
||||
},
|
||||
});
|
5
Props/client/src/views/About.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
69
Props/client/src/views/Authentication.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="info">
|
||||
<WaitCircle class="text-primary">
|
||||
<span class="my-1 text-muted"
|
||||
>Hang on just a sec! Waiting for authentication stuff to
|
||||
finish.</span
|
||||
>
|
||||
</WaitCircle>
|
||||
<div class="my-1 text-muted" v-show="longTimePassed">
|
||||
This seems to be taking longer than expected...
|
||||
<router-link to="/">Return home</router-link>?
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WaitCircle from "../components/WaitCircle.vue";
|
||||
import { userManager } from "../services/authentication";
|
||||
|
||||
export default {
|
||||
name: "Authentication",
|
||||
props: {
|
||||
action: String
|
||||
},
|
||||
components: {
|
||||
WaitCircle
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
longTimePassed: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => (this.longTimePassed = true), 5000);
|
||||
this.completeCallback();
|
||||
},
|
||||
methods: {
|
||||
async completeCallback() {
|
||||
console.log("Completing callback for " + this.action);
|
||||
if (this.action === "login-callback") {
|
||||
const user = await userManager.signinRedirectCallback();
|
||||
this.$store.dispatch("completeAuthentication", { user });
|
||||
} else if (this.action === "logout-callback") {
|
||||
await userManager.signoutRedirectCallback();
|
||||
this.$store.dispatch("completeDeauthentication");
|
||||
} else if (this.action === "silent-login") {
|
||||
this.$store.dispatch("attemptSilentAuthentication", { redirect: true });
|
||||
} else {
|
||||
console.warn("Unknown callback: " + this.action);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async action() {
|
||||
this.completeCallback();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
156
Props/client/src/views/Home.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div id="main" class="jumbotron d-flex flex-column align-items-center">
|
||||
<div>
|
||||
<img
|
||||
id="large-logo"
|
||||
alt="SHAID logo"
|
||||
src="../assets/images/logo.svg"
|
||||
class="img-fluid"
|
||||
/>
|
||||
</div>
|
||||
<div id="description" class="text-center px-3 my-2">
|
||||
<h1 class="my-2">Props</h1>
|
||||
<p>
|
||||
{{ content?.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="jumbotron sub">
|
||||
<div
|
||||
id="help"
|
||||
class="container d-flex flex-column align-items-center my-3"
|
||||
>
|
||||
<div class="large"><i class="bi bi-search"></i></div>
|
||||
<h2 class="mb-3 mt-4">{{ content?.help.title }}</h2>
|
||||
<SearchBar
|
||||
id="searchbar"
|
||||
placeholder="Search here to get started!"
|
||||
class="my-4"
|
||||
style="width: 100%;"
|
||||
></SearchBar>
|
||||
<p>
|
||||
{{ content?.help.searchIntroduction }}
|
||||
</p>
|
||||
<p>
|
||||
{{ content?.help.additionalInfo }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="features"
|
||||
class="container d-flex flex-column align-items-center my-3"
|
||||
>
|
||||
<h2 class="mb-3 mt-4">Features</h2>
|
||||
<p>
|
||||
{{ content?.features.description }}
|
||||
</p>
|
||||
<div class="overflow-auto">
|
||||
<div v-if="content" class="row my-2">
|
||||
<InfoCard
|
||||
v-for="feature in content.features.list"
|
||||
v-bind:key="feature"
|
||||
style="width: 32rem"
|
||||
>
|
||||
<template v-slot:title>{{ feature.title }}</template>
|
||||
<template v-slot:subtitle>{{ feature.subtitle }}</template>
|
||||
<template v-slot:text>{{ feature.text }}</template>
|
||||
</InfoCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import InfoCard from "../components/InfoCard.vue";
|
||||
import SearchBar from "../components/SearchBar.vue";
|
||||
|
||||
export default {
|
||||
name: "Home",
|
||||
components: {
|
||||
InfoCard,
|
||||
SearchBar
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
content: {
|
||||
description:
|
||||
"Props is a site designed to help with the online project component shopping experience. Create project component lists and search across multiple commonly used online retail stores to find the ideal purchase.",
|
||||
help: {
|
||||
title: "Getting Started",
|
||||
searchIntroduction:
|
||||
"Props is a site designed to help with the online project component shopping experience. Create project component lists and search across multiple commonly used online retail stores to find the ideal purchase.",
|
||||
additionalInfo:
|
||||
"Take advantage of our project component manager, where you can add detailed descriptions of what things are for, as well as overall project summaries."
|
||||
},
|
||||
features: {
|
||||
description:
|
||||
"Props strives to be a platform where people can find and organize the material they need to complete their projects. Our features are therefore tailored to the community helping each other, as well as smart tools to quickly compare different products from different stores.",
|
||||
list: [
|
||||
{
|
||||
title: "Shopping List",
|
||||
subtitle: "We'll do it so you don't need to.",
|
||||
text:
|
||||
"We'll help you track the components you need. You can add descriptions, and mark things as purchased, or shipped at your convenience. You can even add components from stores that Props doesn't know about yet!"
|
||||
},
|
||||
{
|
||||
title: "Product Comparison",
|
||||
subtitle: "So many shops to look through...",
|
||||
text:
|
||||
"There's so many online retailers nowadays that it's becoming more and more work to check all of them for what you need. With us, we'll do the searching for you. All you need to do is decide if the shipping time, cost, ratings and reviews are suitable!"
|
||||
},
|
||||
{
|
||||
title: "Auto Listing Search",
|
||||
subtitle: "Need a starting point?",
|
||||
text:
|
||||
"Some project parts commonly used. We'll try and find these parts for you automatically. This means you can create a shopping list, and we'll try and the best fitting products for you based on your shopping list entries."
|
||||
},
|
||||
{
|
||||
title: "Share It!",
|
||||
subtitle: "Show off your work!",
|
||||
text:
|
||||
"Have a project that you're excited about? Want to tell all your friends? We'll help. Each of your projects has a privacy setting. You can have it publically listed, unlisted, or completely private. Short link included."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#large-logo {
|
||||
max-height: 270px;
|
||||
}
|
||||
|
||||
.overflow-auto {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex-wrap: nowrap;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
#help {
|
||||
max-width: 810px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
#description {
|
||||
max-width: 540px;
|
||||
}
|
||||
|
||||
.large {
|
||||
.bi {
|
||||
font-size: 5em;
|
||||
}
|
||||
}
|
||||
</style>
|
10
Props/client/tests/e2e/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
plugins: ['wdio'],
|
||||
extends: 'plugin:wdio/recommended',
|
||||
env: {
|
||||
mocha: true
|
||||
},
|
||||
rules: {
|
||||
strict: 'off'
|
||||
}
|
||||
}
|
15
Props/client/tests/e2e/pageobjects/app.page.js
Normal file
@@ -0,0 +1,15 @@
|
||||
class App {
|
||||
/**
|
||||
* elements
|
||||
*/
|
||||
get heading () { return $('h1') }
|
||||
|
||||
/**
|
||||
* methods
|
||||
*/
|
||||
open (path = '/') {
|
||||
browser.url(path)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new App()
|
8
Props/client/tests/e2e/specs/app.spec.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const App = require('../pageobjects/app.page')
|
||||
|
||||
describe('Vue.js app', () => {
|
||||
it('should open and render', () => {
|
||||
App.open()
|
||||
expect(App.heading).toHaveText('Welcome to Your Vue.js App')
|
||||
})
|
||||
})
|
13
Props/client/tests/unit/example.spec.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { expect } from 'chai'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import HelloWorld from '@/components/HelloWorld.vue'
|
||||
|
||||
describe('HelloWorld.vue', () => {
|
||||
it('renders props.msg when passed', () => {
|
||||
const msg = 'new message'
|
||||
const wrapper = shallowMount(HelloWorld, {
|
||||
props: { msg }
|
||||
})
|
||||
expect(wrapper.text()).to.include(msg)
|
||||
})
|
||||
})
|
18
Props/client/vue.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
pages: {
|
||||
index: {
|
||||
entry: "src/main.js",
|
||||
template: "public/index.html",
|
||||
title: "Props"
|
||||
}
|
||||
},
|
||||
css: {
|
||||
loaderOptions: {
|
||||
sass: {
|
||||
prependData: `
|
||||
@use "@/assets/scss/_themer.scss";
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
24
Props/client/wdio.local.conf.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const { config } = require('./wdio.shared.conf')
|
||||
|
||||
exports.config = {
|
||||
/**
|
||||
* base config
|
||||
*/
|
||||
...config,
|
||||
/**
|
||||
* config for local testing
|
||||
*/
|
||||
maxInstances: 1,
|
||||
services: ['chromedriver'],
|
||||
capabilities: [
|
||||
{
|
||||
browserName: 'chrome',
|
||||
acceptInsecureCerts: true,
|
||||
'goog:chromeOptions': {
|
||||
args: process.argv.includes('--headless')
|
||||
? ['--headless', '--disable-gpu']
|
||||
: []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
41
Props/client/wdio.sauce.conf.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const { config } = require('./wdio.shared.conf')
|
||||
|
||||
const BUILD_ID = Math.ceil(Date.now() / 1000)
|
||||
|
||||
exports.config = {
|
||||
/**
|
||||
* base config
|
||||
*/
|
||||
...config,
|
||||
/**
|
||||
* config for testing on Sauce Labs
|
||||
*/
|
||||
user: process.env.SAUCE_USERNAME,
|
||||
key: process.env.SAUCE_ACCESS_KEY,
|
||||
region: 'us',
|
||||
headless: process.argv.includes('--headless'),
|
||||
|
||||
services: [
|
||||
['sauce', {
|
||||
sauceConnect: true,
|
||||
tunnelIdentifier: 'Vue.js Integration tests'
|
||||
}]
|
||||
],
|
||||
|
||||
maxInstances: 10,
|
||||
capabilities: [{
|
||||
browserName: 'firefox',
|
||||
browserVersion: 'latest',
|
||||
platformName: 'Windows 10',
|
||||
'sauce:options': {
|
||||
build: `Build ${BUILD_ID}`
|
||||
}
|
||||
}, {
|
||||
browserName: 'chrome',
|
||||
browserVersion: 'latest',
|
||||
platformName: 'Windows 10',
|
||||
'sauce:options': {
|
||||
build: `Build ${BUILD_ID}`
|
||||
}
|
||||
}]
|
||||
}
|
210
Props/client/wdio.shared.conf.js
Normal file
@@ -0,0 +1,210 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
const path = require('path')
|
||||
|
||||
exports.config = {
|
||||
// ==================
|
||||
// Specify Test Files
|
||||
// ==================
|
||||
// Define which test specs should run. The pattern is relative to the directory
|
||||
// from which `wdio` was called. Notice that, if you are calling `wdio` from an
|
||||
// NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
|
||||
// directory is where your package.json resides, so `wdio` will be called from there.
|
||||
//
|
||||
specs: [
|
||||
path.join(__dirname, '/tests/e2e/**/*.spec.js')
|
||||
],
|
||||
// Patterns to exclude.
|
||||
exclude: [
|
||||
// 'test/spec/multibrowser/**',
|
||||
// 'test/spec/mobile/**'
|
||||
],
|
||||
//
|
||||
// ===================
|
||||
// Test Configurations
|
||||
// ===================
|
||||
// Define all options that are relevant for the WebdriverIO instance here
|
||||
//
|
||||
// Level of logging verbosity: trace | debug | info | warn | error | silent
|
||||
logLevel: 'trace',
|
||||
//
|
||||
// Set directory to store all logs into
|
||||
outputDir: path.join(__dirname, 'tests/e2e/logs'),
|
||||
//
|
||||
// If you only want to run your tests until a specific amount of tests have failed use
|
||||
// bail (default is 0 - don't bail, run all tests).
|
||||
bail: 0,
|
||||
//
|
||||
// Default timeout for all waitFor* commands.
|
||||
waitforTimeout: 1000,
|
||||
//
|
||||
// Framework you want to run your specs with.
|
||||
// The following are supported: Mocha, Jasmine, and Cucumber
|
||||
// see also: https://webdriver.io/docs/frameworks.html
|
||||
//
|
||||
// Make sure you have the wdio adapter package for the specific framework
|
||||
// installed before running any tests.
|
||||
framework: 'mocha',
|
||||
//
|
||||
// The number of times to retry the entire specfile when it fails as a whole
|
||||
specFileRetries: 1,
|
||||
//
|
||||
// Retried specfiles are inserted at the beginning of the queue and retried immediately
|
||||
specFileRetriesDeferred: false,
|
||||
//
|
||||
// Test reporter for stdout.
|
||||
// The only one supported by default is 'dot'
|
||||
// see also: https://webdriver.io/docs/dot-reporter.html
|
||||
reporters: ['spec'],
|
||||
//
|
||||
// Options to be passed to Mocha.
|
||||
// See the full list at http://mochajs.org/
|
||||
mochaOpts: {
|
||||
ui: 'bdd',
|
||||
timeout: 30000
|
||||
},
|
||||
//
|
||||
// =====
|
||||
// Hooks
|
||||
// =====
|
||||
// WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
|
||||
// it and to build services around it. You can either apply a single function or an array of
|
||||
// methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
|
||||
// resolved to continue.
|
||||
//
|
||||
/**
|
||||
* Gets executed once before all workers get launched.
|
||||
* @param {Object} config wdio configuration object
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
*/
|
||||
onPrepare: function (config, capabilities) {
|
||||
},
|
||||
/**
|
||||
* Gets executed before a worker process is spawned and can be used to initialise specific service
|
||||
* for that worker as well as modify runtime environments in an async fashion.
|
||||
* @param {String} cid capability id (e.g 0-0)
|
||||
* @param {[type]} caps object containing capabilities for session that will be spawn in the worker
|
||||
* @param {[type]} specs specs to be run in the worker process
|
||||
* @param {[type]} args object that will be merged with the main configuration once worker is initialised
|
||||
* @param {[type]} execArgv list of string arguments passed to the worker process
|
||||
*/
|
||||
onWorkerStart: function (cid, caps, specs, args, execArgv) {
|
||||
},
|
||||
/**
|
||||
* Gets executed just before initialising the webdriver session and test framework. It allows you
|
||||
* to manipulate configurations depending on the capability or spec.
|
||||
* @param {Object} config wdio configuration object
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {Array.<String>} specs List of spec file paths that are to be run
|
||||
*/
|
||||
beforeSession: function (config, capabilities, specs) {
|
||||
},
|
||||
/**
|
||||
* Gets executed before test execution begins. At this point you can access to all global
|
||||
* variables like `browser`. It is the perfect place to define custom commands.
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {Array.<String>} specs List of spec file paths that are to be run
|
||||
*/
|
||||
before: function (capabilities, specs) {
|
||||
},
|
||||
/**
|
||||
* Hook that gets executed before the suite starts
|
||||
* @param {Object} suite suite details
|
||||
*/
|
||||
beforeSuite: function (suite) {
|
||||
},
|
||||
/**
|
||||
* Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
|
||||
* beforeEach in Mocha)
|
||||
* stepData and world are Cucumber framework specific
|
||||
*/
|
||||
beforeHook: function (test, context/*, stepData, world */) {
|
||||
},
|
||||
/**
|
||||
* Hook that gets executed _after_ a hook within the suite ends (e.g. runs after calling
|
||||
* afterEach in Mocha)
|
||||
* stepData and world are Cucumber framework specific
|
||||
*/
|
||||
afterHook: function (test, context, { error, result, duration, passed, retries }/*, stepData, world */) {
|
||||
},
|
||||
/**
|
||||
* Function to be executed before a test (in Mocha/Jasmine) starts.
|
||||
*/
|
||||
beforeTest: function (test, context) {
|
||||
},
|
||||
//
|
||||
/**
|
||||
* Runs before a WebdriverIO command gets executed.
|
||||
* @param {String} commandName command name
|
||||
* @param {Array} args arguments that command would receive
|
||||
*/
|
||||
beforeCommand: function (commandName, args) {
|
||||
},
|
||||
/**
|
||||
* Runs after a WebdriverIO command gets executed.
|
||||
* @param {String} commandName hook command name
|
||||
* @param {Array} args arguments that command would receive
|
||||
* @param {Number} result 0 - command success, 1 - command error
|
||||
* @param {Object} error error object if any
|
||||
*/
|
||||
afterCommand: function (commandName, args, result, error) {
|
||||
},
|
||||
/**
|
||||
* Function to be executed after a test (in Mocha/Jasmine) ends.
|
||||
*/
|
||||
afterTest: function (test, context, { error, result, duration, passed, retries }) {
|
||||
},
|
||||
/**
|
||||
* Hook that gets executed after the suite has ended
|
||||
* @param {Object} suite suite details
|
||||
*/
|
||||
afterSuite: function (suite) {
|
||||
},
|
||||
/**
|
||||
* Gets executed after all tests are done. You still have access to all global variables from
|
||||
* the test.
|
||||
* @param {Number} result 0 - test pass, 1 - test fail
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {Array.<String>} specs List of spec file paths that ran
|
||||
*/
|
||||
after: function (result, capabilities, specs) {
|
||||
},
|
||||
/**
|
||||
* Gets executed right after terminating the webdriver session.
|
||||
* @param {Object} config wdio configuration object
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {Array.<String>} specs List of spec file paths that ran
|
||||
*/
|
||||
afterSession: function (config, capabilities, specs) {
|
||||
},
|
||||
/**
|
||||
* Gets executed after all workers got shut down and the process is about to exit. An error
|
||||
* thrown in the onComplete hook will result in the test run failing.
|
||||
* @param {Object} exitCode 0 - success, 1 - fail
|
||||
* @param {Object} config wdio configuration object
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {<Object>} results object containing test results
|
||||
*/
|
||||
onComplete: function (exitCode, config, capabilities, results) {
|
||||
},
|
||||
/**
|
||||
* Gets executed when a refresh happens.
|
||||
* @param {String} oldSessionId session ID of the old session
|
||||
* @param {String} newSessionId session ID of the new session
|
||||
*/
|
||||
onReload: function (oldSessionId, newSessionId) {
|
||||
},
|
||||
//
|
||||
// Cucumber specific hooks
|
||||
beforeFeature: function (uri, feature, scenarios) {
|
||||
},
|
||||
beforeScenario: function (uri, feature, scenario, sourceLocation) {
|
||||
},
|
||||
beforeStep: function ({ uri, feature, step }, context) {
|
||||
},
|
||||
afterStep: function ({ uri, feature, step }, context, { error, result, duration, passed, retries }) {
|
||||
},
|
||||
afterScenario: function (uri, feature, scenario, result, sourceLocation) {
|
||||
},
|
||||
afterFeature: function (uri, feature, scenarios) {
|
||||
}
|
||||
}
|