initial commit, have most of the categories crud methods

This commit is contained in:
Annika
2022-07-01 16:02:45 -06:00
commit a943a20d25
104 changed files with 34031 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
> 1%
last 2 versions
+5
View File
@@ -0,0 +1,5 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
+34
View File
@@ -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
}
}
+20
View File
@@ -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?
+14
View File
@@ -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"
}
]
}
+4
View File
@@ -0,0 +1,4 @@
declare module "*.vue" {
import Vue from 'vue';
export default Vue.defineComponent();
}
+16
View File
@@ -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">
+24
View File
@@ -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>
budgit.client
</title>
</head>
<body>
<noscript>
<strong>We're sorry but budgit.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>
+5
View File
@@ -0,0 +1,5 @@
{
"include": [
"./src/**/*"
]
}
+6269
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
{
"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",
"autoprefixer": "^10.4.7",
"eslint": "8.0.1",
"eslint-plugin-import": "2.25.2",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-vue": "7.19.1",
"postcss": "^8.4.14",
"sass": "1.32.4",
"tailwindcss": "^3.1.4",
"vite": "2.6.10"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+29
View File
@@ -0,0 +1,29 @@
<template>
<header>
<Navbar />
</header>
<main>
<router-view />
</main>
<footer>
<div>
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="postcss">
</style>
+7
View File
@@ -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,
);
+37
View File
@@ -0,0 +1,37 @@
@import "./_variables.scss";
@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;
}
+84
View File
@@ -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>
+56
View File
@@ -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>
+6
View File
@@ -0,0 +1,6 @@
export const dev = window.location.origin.includes('localhost')
export const baseURL = dev ? 'http://localhost:3000' : ''
export const useSockets = false
export const domain = 'dev-h9kk6tgd.us.auth0.com'
export const clientId = 'C1FImfihED3KA1bZk7F3JsGg5CyjwrdX'
export const audience = 'https://AnniDev.com'
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+14
View File
@@ -0,0 +1,14 @@
import '@mdi/font/css/materialdesignicons.css'
import './index.css'
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')
+11
View File
@@ -0,0 +1,11 @@
<template>
<div class="about">
<h1>This is the about page</h1>
</div>
</template>
<script>
export default {
name: 'AboutPage'
}
</script>
+26
View File
@@ -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>
+36
View File
@@ -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)
})
}
+32
View File
@@ -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()
+45
View File
@@ -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()
+35
View File
@@ -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)
}
}
+74
View File
@@ -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')
}
}
+80
View File
@@ -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)
}
}
+11
View File
@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./index.html",
"./src/**/*.vue"
],
theme: {
extend: {},
},
plugins: [],
}
+14
View File
@@ -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
}
})