init
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
@@ -0,0 +1,5 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
@@ -0,0 +1,34 @@
|
||||
const ALWAYS = 'always'
|
||||
const NEVER = 'never'
|
||||
const ERROR = 2
|
||||
const WARN = 1
|
||||
const OFF = 0
|
||||
const PROD = process.env.NODE_ENV === 'production'
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/vue3-strongly-recommended',
|
||||
'@vue/standard'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': PROD ? ERROR : WARN,
|
||||
'no-console': PROD ? ERROR : WARN,
|
||||
'no-debugger': PROD ? ERROR : WARN,
|
||||
'space-before-function-paren': [WARN, NEVER],
|
||||
'vue/max-attributes-per-line': [ERROR, {
|
||||
singleline: 5,
|
||||
multiline: {
|
||||
max: 1,
|
||||
allowFirstLine: true
|
||||
}
|
||||
}],
|
||||
'vue/html-self-closing': OFF
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "npm run serve",
|
||||
"name": "Start Client",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare module "*.vue" {
|
||||
import Vue from 'vue';
|
||||
export default Vue.defineComponent();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
CodeWorks Vue Starter
|
||||
=====================
|
||||
This template is designed to help get students started building vue applications
|
||||
|
||||
## Legal Overview
|
||||
|
||||
The content under the CodeWorks®, LLC Organization and all of the individual repos are solely intended for use by CodeWorks Instruction to deliver Educational content to CodeWorks Students.
|
||||
|
||||
---
|
||||
|
||||
## Copyright
|
||||
|
||||
© CodeWorks® LLC, 2021. Unauthorized use and/or duplication of this material without express and written permission from CodeWorks, LLC is strictly prohibited.
|
||||
|
||||
|
||||
<img src="https://bcw.blob.core.windows.net/public/img/7815839041305055" width="125">
|
||||
@@ -0,0 +1,24 @@
|
||||
<!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" />
|
||||
<link rel="icon" href="https://bcw.blob.core.windows.net/public/img/9977764104160066" />
|
||||
<title>
|
||||
keepr.client
|
||||
</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but keepr.client doesn't work properly without JavaScript
|
||||
enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script type="module" src="./src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
]
|
||||
}
|
||||
Generated
+5253
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "cw-vue-template",
|
||||
"version": "0.1.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bcwdev/auth0provider-client": "^0.0.12",
|
||||
"@mdi/font": "^6.3.95",
|
||||
"@popperjs/core": "^2.10.2",
|
||||
"axios": "^0.23.0",
|
||||
"bootstrap": "^5.1.3",
|
||||
"socket.io-client": "^4.1.2",
|
||||
"sweetalert2": "^11.1.9",
|
||||
"vue": "^3.2.11",
|
||||
"vue-router": "^4.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "1.9.3",
|
||||
"@vue/compiler-sfc": "3.2.20",
|
||||
"eslint": "8.0.1",
|
||||
"eslint-plugin-import": "2.25.2",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-vue": "7.19.1",
|
||||
"sass": "1.32.4",
|
||||
"vite": "2.6.10"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<header>
|
||||
<Navbar />
|
||||
</header>
|
||||
<main>
|
||||
<router-view />
|
||||
</main>
|
||||
<footer>
|
||||
<div class="bg-dark text-light text-center p-4">
|
||||
Made with 💖 by CodeWorks
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import { AppState } from './AppState'
|
||||
export default {
|
||||
name: 'App',
|
||||
setup() {
|
||||
return {
|
||||
appState: computed(() => AppState)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "./assets/scss/main.scss";
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { reactive } from 'vue'
|
||||
|
||||
// NOTE AppState is a reactive object to contain app level data
|
||||
export const AppState = reactive({
|
||||
user: {},
|
||||
account: {}
|
||||
})
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
@@ -0,0 +1,79 @@
|
||||
.skeleton-loader {
|
||||
box-shadow: 0 4px 10px 0 rgba(33, 33, 33, 0.15);
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: #dcdcdc;
|
||||
pointer-events: none;
|
||||
&.dark{
|
||||
background-color: #9d9c9c;
|
||||
}
|
||||
|
||||
&.card{
|
||||
background-color: #fff;
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, transparent 0%, #f6f6f6 50%, transparent 100%);
|
||||
animation: load 1.5s cubic-bezier(0.4, 0.0, 0.2, 1) infinite;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
&.avatar{
|
||||
border-radius: 50%;
|
||||
height: 45px;
|
||||
width: 45px;
|
||||
&.tile{
|
||||
border-radius: 5%;
|
||||
}
|
||||
&.md{
|
||||
height: 65px;
|
||||
width: 65px;
|
||||
}
|
||||
&.lg{
|
||||
height: 85px;
|
||||
width: 85px;
|
||||
}
|
||||
}
|
||||
&.text{
|
||||
width: clamp(50%, 160px, 100%);
|
||||
height: 1rem;
|
||||
line-height: 1.5rem;
|
||||
margin: .5rem 0;
|
||||
&.sm{
|
||||
width: clamp(25%, 80px, 100%);
|
||||
}
|
||||
&.lg{
|
||||
width: clamp(75%, 250px, 100%);
|
||||
}
|
||||
&.block{
|
||||
width: clamp(100%, 500px, 100%);
|
||||
}
|
||||
}
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 50px;
|
||||
background: linear-gradient(to right, transparent 0%, #E8E8E8 50%, transparent 100%);
|
||||
animation: load 1s cubic-bezier(0.4, 0.0, 0.2, 1) infinite;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes load {
|
||||
from {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.form-control {
|
||||
background-color: #f7f5f5;
|
||||
&:focus {
|
||||
background-color: #fbf9f9;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Transitions
|
||||
|
||||
// [+] ROUTER TRANSITION
|
||||
// intentially empty by default
|
||||
// if you want to enable router
|
||||
// transtions change the settings below
|
||||
.route-enter-active,
|
||||
.route-leave-active {
|
||||
transition: all 0s linear; // try changing 0s to .25s
|
||||
}
|
||||
.route-enter-from,
|
||||
.route-leave-to {
|
||||
opacity: 1; // try setting to 0
|
||||
}
|
||||
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
.slideY-enter-active,
|
||||
.slideY-leave-active {
|
||||
transform: translateY(0);
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
.slideY-enter-from,
|
||||
.slideY-leave-to {
|
||||
transform: translateY(100vh);
|
||||
}
|
||||
.slideX-enter-active,
|
||||
.slideX-leave-active {
|
||||
transform: translateX(0);
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
.slideX-enter-from,
|
||||
.slideX-leave-to {
|
||||
transform: translateX(100vw);
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
@function color-yiq($background) {
|
||||
$r: red($background);
|
||||
$g: green($background);
|
||||
$b: blue($background);
|
||||
|
||||
$yiq: (($r * 299) + ($g * 587) + ($b * 114))/1000;
|
||||
|
||||
@if ($yiq >= 128) {
|
||||
@return #111;
|
||||
} @else {
|
||||
@return #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@function fade($background, $alpha) {
|
||||
$r: red($background);
|
||||
$g: green($background);
|
||||
$b: blue($background);
|
||||
@return rgba($r, $g, $b, $alpha);
|
||||
}
|
||||
|
||||
@each $color, $value in $colors {
|
||||
.bg-#{$color} {
|
||||
color: color-yiq($value);
|
||||
background-color: $value !important;
|
||||
}
|
||||
.text-#{$color} {
|
||||
color: $value !important;
|
||||
}
|
||||
$num: 5;
|
||||
@while $num < 100 {
|
||||
$darker: darken($value, $num);
|
||||
$lighter: lighten($value, $num);
|
||||
.bg-#{$color} {
|
||||
&.lighten-#{$num} {
|
||||
color: color-yiq($lighter);
|
||||
background-color: $lighter !important;
|
||||
}
|
||||
&.darken-#{$num} {
|
||||
color: color-yiq($darker);
|
||||
background-color: $darker !important;
|
||||
}
|
||||
&.fade-#{$num} {
|
||||
background-color: fade($value, $num / 100) !important;
|
||||
}
|
||||
&.fade-lighten-#{$num} {
|
||||
background-color: fade($lighter, $num / 100) !important;
|
||||
}
|
||||
&.fade-darken-#{$num} {
|
||||
background-color: fade($darker, $num / 100) !important;
|
||||
}
|
||||
}
|
||||
.text-#{$color}.darken-#{$num} {
|
||||
color: $darker !important;
|
||||
}
|
||||
|
||||
.text-#{$color}.lighten-#{$num} {
|
||||
color: $lighter !important;
|
||||
}
|
||||
$num: $num + 5;
|
||||
}
|
||||
}
|
||||
|
||||
.clip-text {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.action {
|
||||
cursor: pointer;
|
||||
}
|
||||
.no-select {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.scrollable-x {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.scrollable-y {
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
// opacity: 0.65;
|
||||
&::after {
|
||||
cursor: not-allowed;
|
||||
pointer-events: all;
|
||||
position: absolute;
|
||||
content: "";
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.25rem;
|
||||
text-shadow: 1px 1px 0 #555, -1px -1px 0 #555, 1px -1px 0 #555, -1px 1px 0 #555, 1px 1px 0 #555;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(85, 85, 85, 0.37);
|
||||
&.text-disabled {
|
||||
content: "DISABLED";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.on-hover {
|
||||
transition: all 0.2s linear;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
*:hover > .on-hover {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.square {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.square-left {
|
||||
border-top-left-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
.square-right {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
.square-top {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
}
|
||||
.square-bottom {
|
||||
border-bottom-right-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
.square-bottom-left {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
.square-bottom-right {
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
.square-top-left {
|
||||
border-top-left-radius: 0 !important;
|
||||
}
|
||||
.square-top-right {
|
||||
border-top-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
.selectable {
|
||||
position: relative;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
&:after {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-radius: inherit;
|
||||
bottom: 0;
|
||||
color: inherit;
|
||||
content: "";
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: opacity 0.2s cubic-bezier(0.4, 0, 0.6, 1);
|
||||
background-color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.elevation-1 {
|
||||
box-shadow: 0 2px 1px -1px rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 1px 3px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.elevation-2 {
|
||||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.elevation-3 {
|
||||
box-shadow: 0 3px 3px -2px rgba(0, 0, 0, 0.2), 0 3px 4px 0 rgba(0, 0, 0, 0.14), 0 1px 8px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.elevation-4 {
|
||||
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.elevation-5 {
|
||||
box-shadow: 0 2px 5px -1px rgba(0, 0, 0, 0.2), 0 5px 6px 0 rgba(0, 0, 0, 0.14), 0 1px 12px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.f-8 {
|
||||
font-size: 8pt !important;
|
||||
}
|
||||
.f-10 {
|
||||
font-size: 10pt !important;
|
||||
}
|
||||
.f-12 {
|
||||
font-size: 12pt !important;
|
||||
}
|
||||
.f-14 {
|
||||
font-size: 14pt !important;
|
||||
}
|
||||
.f-16 {
|
||||
font-size: 16pt !important;
|
||||
}
|
||||
.f-18 {
|
||||
font-size: 18pt !important;
|
||||
}
|
||||
.f-20 {
|
||||
font-size: 20pt !important;
|
||||
}
|
||||
.f-22 {
|
||||
font-size: 22pt !important;
|
||||
}
|
||||
.f-24 {
|
||||
font-size: 24pt !important;
|
||||
}
|
||||
.f-26 {
|
||||
font-size: 26pt !important;
|
||||
}
|
||||
.f-28 {
|
||||
font-size: 28pt !important;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.f-8 {
|
||||
font-size: 0.65rem !important;
|
||||
}
|
||||
|
||||
.f-10 {
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
|
||||
.f-12 {
|
||||
font-size: 0.85rem !important;
|
||||
}
|
||||
|
||||
.f-14 {
|
||||
font-size: 0.95rem !important;
|
||||
}
|
||||
|
||||
.f-16 {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
.f-18 {
|
||||
font-size: 1.15rem !important;
|
||||
}
|
||||
|
||||
.f-20 {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
|
||||
.f-22 {
|
||||
font-size: 1.34rem !important;
|
||||
}
|
||||
|
||||
.f-24 {
|
||||
font-size: 1.45rem !important;
|
||||
}
|
||||
|
||||
.f-26 {
|
||||
font-size: 1.65rem !important;
|
||||
}
|
||||
|
||||
.f-28 {
|
||||
font-size: 1.75rem !important;
|
||||
}
|
||||
|
||||
.f-30 {
|
||||
font-size: 2rem !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
$body-bg: #e5e5e5;
|
||||
$primary: #3ba5dc;
|
||||
$secondary: #8392ab;
|
||||
$success: #00f867;
|
||||
$info: #5d0149;
|
||||
$warning: #fbcf33;
|
||||
$danger: #ea0606;
|
||||
$light: #e9ecef;
|
||||
$dark: #111927;
|
||||
$grey: #cbcbcb;
|
||||
|
||||
$font-sans-serif: Open Sans;
|
||||
$font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
$gradient: linear-gradient(180deg, hsla(0, 0%, 100%, 0.15), hsla(0, 0%, 100%, 0));
|
||||
|
||||
$colors: (
|
||||
"primary": $primary,
|
||||
"secondary": $secondary,
|
||||
"danger": $danger,
|
||||
"success": $success,
|
||||
"warning": $warning,
|
||||
"info": $info,
|
||||
"dark": $dark,
|
||||
"grey": $grey,
|
||||
);
|
||||
|
||||
$color-ranges: (
|
||||
0: 0,
|
||||
5: 5,
|
||||
10: 10,
|
||||
15: 15,
|
||||
20: 20,
|
||||
25: 25,
|
||||
30: 30,
|
||||
35: 35,
|
||||
40: 40,
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
@import "./_variables.scss";
|
||||
@import "bootstrap";
|
||||
@import "./_overrides.scss";
|
||||
@import "./utilities.scss";
|
||||
@import "./loaders.scss";
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background-color: $body-bg;
|
||||
}
|
||||
|
||||
main {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
&:not(:placeholder-shown) {
|
||||
&:valid {
|
||||
border: 1px solid $success;
|
||||
}
|
||||
&:invalid {
|
||||
border: 1px solid $danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form:invalid button[type="submit"] {
|
||||
filter: grayscale(1);
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<span class="navbar-text">
|
||||
<button
|
||||
class="btn selectable text-success lighten-30 text-uppercase my-2 my-lg-0"
|
||||
@click="login"
|
||||
v-if="!user.isAuthenticated"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
|
||||
<div class="dropdown my-2 my-lg-0" v-else>
|
||||
<div
|
||||
class="dropdown-toggle selectable"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
id="authDropdown"
|
||||
>
|
||||
<div v-if="account.picture">
|
||||
<img
|
||||
:src="account.picture"
|
||||
alt="account photo"
|
||||
height="40"
|
||||
class="rounded"
|
||||
/>
|
||||
<span class="mx-3 text-success lighten-30">{{ account.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu p-0 list-group w-100"
|
||||
aria-labelledby="authDropdown"
|
||||
>
|
||||
<router-link :to="{ name: 'Account' }">
|
||||
<div class="list-group-item list-group-item-action hoverable">
|
||||
Manage Account
|
||||
</div>
|
||||
</router-link>
|
||||
<div
|
||||
class="list-group-item list-group-item-action hoverable text-danger"
|
||||
@click="logout"
|
||||
>
|
||||
<i class="mdi mdi-logout"></i>
|
||||
logout
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { computed } from "@vue/reactivity";
|
||||
import { AppState } from "../AppState";
|
||||
import { AuthService } from "../services/AuthService";
|
||||
export default {
|
||||
setup() {
|
||||
return {
|
||||
user: computed(() => AppState.user),
|
||||
account: computed(() => AppState.account),
|
||||
async login() {
|
||||
AuthService.loginWithPopup();
|
||||
},
|
||||
async logout() {
|
||||
AuthService.logout({ returnTo: window.location.origin });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropdown-menu {
|
||||
user-select: none;
|
||||
display: block;
|
||||
transform: scale(0);
|
||||
transition: all 0.15s linear;
|
||||
}
|
||||
.dropdown-menu.show {
|
||||
transform: scale(1);
|
||||
}
|
||||
.hoverable {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark px-3">
|
||||
<router-link class="navbar-brand d-flex" :to="{ name: 'Home' }">
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<img alt="logo" src="../assets/img/cw-logo.png" height="45" />
|
||||
</div>
|
||||
</router-link>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarText"
|
||||
aria-controls="navbarText"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span class="navbar-toggler-icon" />
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarText">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li>
|
||||
<router-link
|
||||
:to="{ name: 'About' }"
|
||||
class="btn text-success lighten-30 selectable text-uppercase"
|
||||
>
|
||||
About
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- LOGIN COMPONENT HERE -->
|
||||
<Login />
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.nav-link {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.navbar-nav .router-link-exact-active {
|
||||
border-bottom: 2px solid var(--bs-success);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
export const dev = window.location.origin.includes('localhost')
|
||||
export const baseURL = dev ? 'https://localhost:5001' : ''
|
||||
export const useSockets = false
|
||||
export const domain = 'dev-h9kk6tgd.us.auth0.com'
|
||||
export const clientId = 'C1FImfihED3KA1bZk7F3JsGg5CyjwrdX'
|
||||
export const audience = 'https://AnniDev.com'
|
||||
@@ -0,0 +1,14 @@
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import 'bootstrap'
|
||||
import { createApp } from 'vue'
|
||||
// @ts-ignore
|
||||
import App from './App.vue'
|
||||
import { registerGlobalComponents } from './registerGlobalComponents'
|
||||
import { router } from './router'
|
||||
|
||||
const root = createApp(App)
|
||||
registerGlobalComponents(root)
|
||||
|
||||
root
|
||||
.use(router)
|
||||
.mount('#app')
|
||||
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is the about page</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AboutPage'
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="about text-center">
|
||||
<h1>Welcome {{ account.name }}</h1>
|
||||
<img class="rounded" :src="account.picture" alt="" />
|
||||
<p>{{ account.email }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import { AppState } from '../AppState'
|
||||
export default {
|
||||
name: 'Account',
|
||||
setup() {
|
||||
return {
|
||||
account: computed(() => AppState.account)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
img {
|
||||
max-width: 100px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="home flex-grow-1 d-flex flex-column align-items-center justify-content-center">
|
||||
<div class="home-card p-5 bg-white rounded elevation-3">
|
||||
<img src="https://bcw.blob.core.windows.net/public/img/8600856373152463" alt="CodeWorks Logo" class="rounded-circle">
|
||||
<h1 class="my-5 bg-dark text-white p-3 rounded text-center">
|
||||
Vue 3 Starter
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Home'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.home{
|
||||
display: grid;
|
||||
height: 80vh;
|
||||
place-content: center;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
.home-card{
|
||||
width: 50vw;
|
||||
> img{
|
||||
height: 200px;
|
||||
max-width: 200px;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
export function registerGlobalComponents(root) {
|
||||
// https://webpack.js.org/guides/dependency-management/#require-context
|
||||
// @ts-ignore
|
||||
const components = import.meta.globEager('./components/*.vue')
|
||||
Object.entries(components).forEach(([fileName, component]) => {
|
||||
const componentName = component.name || fileName
|
||||
.substr(fileName.lastIndexOf('/') + 1)
|
||||
.replace(/\.\w+$/, '')
|
||||
// Register component on this Vue instance
|
||||
root.component(componentName, component.default)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { authGuard } from '@bcwdev/auth0provider-client'
|
||||
|
||||
function loadPage(page) {
|
||||
return () => import(`./pages/${page}.vue`)
|
||||
}
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: loadPage('HomePage')
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
component: loadPage('AboutPage')
|
||||
},
|
||||
{
|
||||
path: '/account',
|
||||
name: 'Account',
|
||||
component: loadPage('AccountPage'),
|
||||
beforeEnter: authGuard
|
||||
}
|
||||
]
|
||||
|
||||
export const router = createRouter({
|
||||
linkActiveClass: 'router-link-active',
|
||||
linkExactActiveClass: 'router-link-exact-active',
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import { AppState } from '../AppState'
|
||||
import { logger } from '../utils/Logger'
|
||||
import { api } from './AxiosService'
|
||||
|
||||
class AccountService {
|
||||
async getAccount() {
|
||||
try {
|
||||
const res = await api.get('/account')
|
||||
AppState.account = res.data
|
||||
} catch (err) {
|
||||
logger.error('HAVE YOU STARTED YOUR SERVER YET???', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const accountService = new AccountService()
|
||||
@@ -0,0 +1,45 @@
|
||||
import { initialize } from '@bcwdev/auth0provider-client'
|
||||
import { AppState } from '../AppState'
|
||||
import { audience, clientId, domain } from '../env'
|
||||
import { router } from '../router'
|
||||
import { accountService } from './AccountService'
|
||||
import { api } from './AxiosService'
|
||||
import { socketService } from './SocketService'
|
||||
|
||||
export const AuthService = initialize({
|
||||
domain,
|
||||
clientId,
|
||||
audience,
|
||||
useRefreshTokens: true,
|
||||
onRedirectCallback: appState => {
|
||||
router.push(
|
||||
appState && appState.targetUrl
|
||||
? appState.targetUrl
|
||||
: window.location.pathname
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
AuthService.on(AuthService.AUTH_EVENTS.AUTHENTICATED, async function() {
|
||||
api.defaults.headers.authorization = AuthService.bearer
|
||||
api.interceptors.request.use(refreshAuthToken)
|
||||
AppState.user = AuthService.user
|
||||
await accountService.getAccount()
|
||||
socketService.authenticate(AuthService.bearer)
|
||||
// NOTE if there is something you want to do once the user is authenticated, place that here
|
||||
})
|
||||
|
||||
async function refreshAuthToken(config) {
|
||||
if (!AuthService.isAuthenticated) { return config }
|
||||
const expires = AuthService.identity.exp * 1000
|
||||
const expired = expires < Date.now()
|
||||
const needsRefresh = expires < Date.now() + (1000 * 60 * 60 * 12)
|
||||
if (expired) {
|
||||
await AuthService.loginWithPopup()
|
||||
} else if (needsRefresh) {
|
||||
await AuthService.getTokenSilently()
|
||||
api.defaults.headers.authorization = AuthService.bearer
|
||||
socketService.authenticate(AuthService.bearer)
|
||||
}
|
||||
return config
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import Axios from 'axios'
|
||||
import { baseURL } from '../env'
|
||||
export const api = Axios.create({
|
||||
baseURL,
|
||||
timeout: 8000
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import Pop from '../utils/Pop'
|
||||
import { SocketHandler } from '../utils/SocketHandler'
|
||||
|
||||
class SocketService extends SocketHandler {
|
||||
constructor() {
|
||||
super()
|
||||
this
|
||||
.on('error', this.onError)
|
||||
}
|
||||
|
||||
onError(e) {
|
||||
Pop.toast(e.message, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
export const socketService = new SocketService()
|
||||
@@ -0,0 +1,35 @@
|
||||
import { dev } from '../env'
|
||||
|
||||
function log(type, content) {
|
||||
if (dev) {
|
||||
// eslint-disable-next-line no-console
|
||||
console[type](`[${type}] :: ${new Date().toLocaleTimeString()} :: `, ...content)
|
||||
} else {
|
||||
switch (type) {
|
||||
case 'log':
|
||||
case 'assert':
|
||||
return
|
||||
}
|
||||
// TODO SEND LOGS TO EXTERNAL SERVICE
|
||||
// eslint-disable-next-line no-console
|
||||
console[type](`[${type}] :: ${new Date().toLocaleTimeString()} :: `, ...content)
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
log() {
|
||||
log('log', arguments)
|
||||
},
|
||||
error() {
|
||||
log('error', arguments)
|
||||
},
|
||||
warn() {
|
||||
log('warn', arguments)
|
||||
},
|
||||
assert() {
|
||||
log('assert', arguments)
|
||||
},
|
||||
trace() {
|
||||
log('trace', arguments)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import Swal from 'sweetalert2'
|
||||
import 'sweetalert2/dist/sweetalert2.min.css'
|
||||
|
||||
export default class Pop {
|
||||
/**
|
||||
*
|
||||
* @param {string} title The title text.
|
||||
* @param {string} text The body text.
|
||||
* @param {string} icon 'success', 'error', 'info', 'warning', or 'question'.
|
||||
* @param {string} confirmButtonText The text of your confirm button.
|
||||
* -----------------------------------
|
||||
* {@link https://sweetalert2.github.io/#configuration|Check out Sweet Alerts}
|
||||
*/
|
||||
static async confirm(title = 'Are you sure?', text = "You won't be able to revert this!", icon = 'warning', confirmButtonText = 'Yes, delete it!') {
|
||||
try {
|
||||
const res = await Swal.fire({
|
||||
title: title,
|
||||
text: text,
|
||||
icon: icon,
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: confirmButtonText
|
||||
})
|
||||
if (res.isConfirmed) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} title The title text
|
||||
* @param {string} display 'success', 'error', 'info', 'warning', or 'question'.
|
||||
* @param {string} position 'top', 'top-start', 'top-end', 'center', 'center-start', 'center-end', 'bottom', 'bottom-start', or 'bottom-end'.
|
||||
* @param {number} timer Time in milliseconds.
|
||||
* @param {boolean} progressBar Show progress bar or not respectively.
|
||||
* -----------------------------------
|
||||
* {@link https://sweetalert2.github.io/#configuration|Check out Sweet Alerts}
|
||||
*/
|
||||
static toast(title = 'Warning!', display = 'warning', position = 'top-end', timer = 3000, progressBar = true) {
|
||||
Swal.fire({
|
||||
title: title,
|
||||
icon: display,
|
||||
position: position,
|
||||
timer: timer,
|
||||
timerProgressBar: progressBar,
|
||||
toast: true,
|
||||
showConfirmButton: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('axios').AxiosError | Error | String } Error An Error Object.
|
||||
*/
|
||||
static error(error) {
|
||||
if (error.isAxiosError) {
|
||||
const { response } = error
|
||||
this.toast(response.data.error?.message || response.data.message, 'error')
|
||||
} else {
|
||||
this.toast(error.message || error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { String } message The message to display. If not provided, will display a generic message.
|
||||
*/
|
||||
static success(message = 'Success!') {
|
||||
this.toast(message, 'success')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { io } from 'socket.io-client'
|
||||
import { baseURL, useSockets } from '../env.js'
|
||||
import { logger } from './Logger.js'
|
||||
|
||||
const SOCKET_EVENTS = {
|
||||
connection: 'connection',
|
||||
connected: 'connected',
|
||||
disconnect: 'disconnect',
|
||||
authenticate: 'authenticate',
|
||||
authenticated: 'authenticated',
|
||||
userConnected: 'userConnected',
|
||||
userDisconnected: 'userDisconnected',
|
||||
error: 'error'
|
||||
}
|
||||
|
||||
export class SocketHandler {
|
||||
/**
|
||||
* @param {String} url
|
||||
*/
|
||||
constructor(requiresAuth = false, url = baseURL) {
|
||||
if (!useSockets) { return }
|
||||
this.socket = io(url || baseURL)
|
||||
this.requiresAuth = requiresAuth
|
||||
this.queue = []
|
||||
this.authenticated = false
|
||||
this
|
||||
.on(SOCKET_EVENTS.connected, this.onConnected)
|
||||
.on(SOCKET_EVENTS.authenticated, this.onAuthenticated)
|
||||
.on(SOCKET_EVENTS.error, this.onError)
|
||||
}
|
||||
|
||||
on(event, fn) {
|
||||
this.socket?.on(event, fn.bind(this))
|
||||
return this
|
||||
}
|
||||
|
||||
onConnected(connection) {
|
||||
logger.log('[SOCKET_CONNECTION]', connection)
|
||||
this.connected = true
|
||||
this.playback()
|
||||
}
|
||||
|
||||
onAuthenticated(auth) {
|
||||
logger.log('[SOCKET_AUTHENTICATED]', auth)
|
||||
this.authenticated = true
|
||||
this.playback()
|
||||
}
|
||||
|
||||
authenticate(bearerToken) {
|
||||
this.socket?.emit(SOCKET_EVENTS.authenticate, bearerToken)
|
||||
}
|
||||
|
||||
onError(error) {
|
||||
logger.error('[SOCKET_ERROR]', error)
|
||||
}
|
||||
|
||||
enqueue(action, payload) {
|
||||
logger.log('[ENQUEING_ACTION]', { action, payload })
|
||||
this.queue.push({ action, payload })
|
||||
}
|
||||
|
||||
playback() {
|
||||
logger.log('[SOCKET_PLAYBACK]')
|
||||
const playback = [...this.queue]
|
||||
this.queue = []
|
||||
playback.forEach(e => {
|
||||
this.emit(e.action, e.payload)
|
||||
})
|
||||
}
|
||||
|
||||
emit(action, payload = undefined) {
|
||||
if (this.requiresAuth && !this.authenticated) {
|
||||
return this.enqueue(action, payload)
|
||||
}
|
||||
if (!this.connected) {
|
||||
return this.enqueue(action, payload)
|
||||
}
|
||||
this.socket.emit(action, payload)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
build: {
|
||||
outDir: 'docs',
|
||||
sourcemap: false
|
||||
},
|
||||
server: {
|
||||
port: 8080
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user