Renamed everything from MultiShop to Props.

This commit is contained in:
2021-07-13 00:35:31 -05:00
parent cefd02f202
commit 7e8a398741
114 changed files with 117 additions and 106 deletions

View File

@@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

View 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
View 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
View 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
View 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/).

View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View File

@@ -0,0 +1,5 @@
{
"include": [
"./src/**/*"
]
}

18602
Props/client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
Props/client/package.json Normal file
View 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"
}
}

View 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();

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View 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

View 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>

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow:

76
Props/client/src/App.vue Normal file
View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View 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

View 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;
}

View 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);
}

View File

@@ -0,0 +1,3 @@
$themes: (
"light": ("background": #F4F4F4, "navbar": #FFF7F7, "main": #BDF2D5, "sub": #F2FCFC, "bold": #1E56A0, "text": #1A1A1A),
);

View 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");
}
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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");

View 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);
}
});
}

View 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 };

View 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;

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View File

@@ -0,0 +1,16 @@
import { createStore } from "vuex";
import { identity } from "./identity";
export default createStore({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
identity: identity,
},
});

View File

@@ -0,0 +1,5 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

View 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>

View 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>

View File

@@ -0,0 +1,10 @@
module.exports = {
plugins: ['wdio'],
extends: 'plugin:wdio/recommended',
env: {
mocha: true
},
rules: {
strict: 'off'
}
}

View File

@@ -0,0 +1,15 @@
class App {
/**
* elements
*/
get heading () { return $('h1') }
/**
* methods
*/
open (path = '/') {
browser.url(path)
}
}
module.exports = new App()

View 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')
})
})

View 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)
})
})

View 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";
`
}
}
}
};

View 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']
: []
}
}
]
}

View 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}`
}
}]
}

View 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) {
}
}