Initial commit
TODO: change api.conf URL references to use environment variables and add these variables to the docker-compose configuration for host domain
This commit is contained in:
12
.env.example
Normal file
12
.env.example
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# JWT_SECRET=thisisastring # use if you change from simple bearer to JWT token auth
|
||||||
|
BEARER_TOKEN=thisisastring
|
||||||
|
|
||||||
|
API_PORT=9230
|
||||||
|
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PORT=3306
|
||||||
|
# DB_PORT=3306
|
||||||
|
DB_ROOT_PASSWORD=thisisanotherstring
|
||||||
|
DB_DATABASE=17thCoreData
|
||||||
|
DB_USER=apiuser
|
||||||
|
DB_PASSWORD=thisisathirdstring
|
||||||
135
.gitignore
vendored
Normal file
135
.gitignore
vendored
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/mysql/**
|
||||||
|
!/mysql/.gitkeep
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
14
17th-web/.eslintrc.cjs
Normal file
14
17th-web/.eslintrc.cjs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
'extends': [
|
||||||
|
'plugin:vue/vue3-essential',
|
||||||
|
'eslint:recommended',
|
||||||
|
'@vue/eslint-config-prettier/skip-formatting'
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest'
|
||||||
|
}
|
||||||
|
}
|
||||||
28
17th-web/.gitignore
vendored
Normal file
28
17th-web/.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
8
17th-web/.prettierrc.json
Normal file
8
17th-web/.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
||||||
3
17th-web/.vscode/extensions.json
vendored
Normal file
3
17th-web/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||||
|
}
|
||||||
35
17th-web/README.md
Normal file
35
17th-web/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# 17th-web
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
20
17th-web/index.html
Normal file
20
17th-web/index.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
|
<link rel="icon" href="./favicon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||||
|
<!-- <link rel="stylesheet" href="./src/style.css"> -->
|
||||||
|
<title>17th Info Site</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
3522
17th-web/package-lock.json
generated
Normal file
3522
17th-web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
17th-web/package.json
Normal file
37
17th-web/package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "17th-web",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
|
||||||
|
"format": "prettier --write src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@headlessui/vue": "^1.7.12",
|
||||||
|
"@heroicons/vue": "^2.0.16",
|
||||||
|
"axios": "^1.3.4",
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
|
"mysql2": "^3.2.0",
|
||||||
|
"pinia": "^2.0.32",
|
||||||
|
"sequelize": "^6.29.3",
|
||||||
|
"vue": "^3.2.47",
|
||||||
|
"vue-router": "^4.1.6",
|
||||||
|
"vuetify": "^3.1.10"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@mdi/font": "^7.2.96",
|
||||||
|
"@rushstack/eslint-patch": "^1.2.0",
|
||||||
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
|
"@vue/eslint-config-prettier": "^7.1.0",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"eslint": "^8.34.0",
|
||||||
|
"eslint-plugin-vue": "^9.9.0",
|
||||||
|
"postcss": "^8.4.21",
|
||||||
|
"prettier": "^2.8.4",
|
||||||
|
"tailwindcss": "^3.2.7",
|
||||||
|
"vite": "^4.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
17th-web/postcss.config.js
Normal file
10
17th-web/postcss.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const tailwindcss = require('tailwindcss');
|
||||||
|
const autoprefixer = require('autoprefixer');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss,
|
||||||
|
autoprefixer,
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
BIN
17th-web/public/favicon.ico
Normal file
BIN
17th-web/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
95
17th-web/src/App.vue
Normal file
95
17th-web/src/App.vue
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<script setup>
|
||||||
|
import { RouterLink, RouterView } from 'vue-router'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-app full-height>
|
||||||
|
<v-card class="m-auto" color="grey-lighten-3">
|
||||||
|
<v-layout>
|
||||||
|
<v-app-bar color="teal-darken-4" image="https://picsum.photos/1920/1080?random" prominent>
|
||||||
|
<template v-slot:image>
|
||||||
|
<v-img gradient="to top right, rgba(19,84,122,.8), rgba(128,208,199,.8)"></v-img>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-app-bar-nav-icon variant="text" @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-app-bar-title>17th Ranger Battalion</v-app-bar-title>
|
||||||
|
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<router-link to="/courses" custom v-slot="{ navigate }">
|
||||||
|
<v-btn icon="mdi-school-outline" @click="navigate"></v-btn>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<router-link to="/ribbons" custom v-slot="{ navigate }">
|
||||||
|
<v-btn icon="mdi-seal-variant" @click="navigate"></v-btn>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<v-btn icon="mdi-dots-vertical"> </v-btn>
|
||||||
|
</v-app-bar>
|
||||||
|
|
||||||
|
<v-navigation-drawer>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
prepend-avatar="https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png"
|
||||||
|
title="17th Admin"
|
||||||
|
subtitle="testuser@example.com"
|
||||||
|
></v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.title"
|
||||||
|
:title="item.title"
|
||||||
|
:prepend-icon="item.icon"
|
||||||
|
:to="item.href"
|
||||||
|
>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
|
||||||
|
<v-main>
|
||||||
|
<v-container fluid>
|
||||||
|
<RouterView />
|
||||||
|
</v-container>
|
||||||
|
</v-main>
|
||||||
|
</v-layout>
|
||||||
|
</v-card>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
components: {
|
||||||
|
RouterView
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
drawer: true,
|
||||||
|
items: [
|
||||||
|
{ title: 'Home', href: '/', icon: 'mdi-home' },
|
||||||
|
{ title: 'Ribbons', href: '/ribbons', icon: 'mdi-seal-variant' },
|
||||||
|
{ title: 'Courses', href: '/courses', icon: 'mdi-school-outline' },
|
||||||
|
{ title: 'About', href: '/about', icon: 'mdi-information-outline' }
|
||||||
|
],
|
||||||
|
rail: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* tailwindcss */
|
||||||
|
/* @import 'tailwindcss/base';
|
||||||
|
|
||||||
|
@import 'tailwindcss/components';
|
||||||
|
|
||||||
|
@import 'tailwindcss/utilities'; */
|
||||||
|
</style>
|
||||||
63
17th-web/src/assets/js/imageUtils.js
Normal file
63
17th-web/src/assets/js/imageUtils.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
export function bufferToBase64 (buf) {
|
||||||
|
var binstr = Array.prototype.map.call(buf, function (ch) {
|
||||||
|
return String.fromCharCode(ch);
|
||||||
|
}).join('');
|
||||||
|
return window.btoa(binstr);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base64ToBuffer (base64) {
|
||||||
|
var binstr = window.atob(base64);
|
||||||
|
var buf = new Uint8Array(binstr.length);
|
||||||
|
Array.prototype.forEach.call(binstr, function (ch, i) {
|
||||||
|
buf[i] = ch.charCodeAt(0);
|
||||||
|
});
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dataURLToBlob (dataURL) {
|
||||||
|
var BASE64_MARKER = ';base64,';
|
||||||
|
if (dataURL.indexOf(BASE64_MARKER) == -1) {
|
||||||
|
var parts = dataURL.split(',');
|
||||||
|
var contentType = parts[0].split(':')[1];
|
||||||
|
var raw = parts[1];
|
||||||
|
|
||||||
|
return new Blob([raw], { type: contentType });
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = dataURL.split(BASE64_MARKER);
|
||||||
|
var contentType = parts[0].split(':')[1];
|
||||||
|
var raw = window.atob(parts[1]);
|
||||||
|
var rawLength = raw.length;
|
||||||
|
|
||||||
|
var uInt8Array = new Uint8Array(rawLength);
|
||||||
|
|
||||||
|
for (var i = 0; i < rawLength; ++i) {
|
||||||
|
uInt8Array[i] = raw.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Blob([uInt8Array], { type: contentType });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dataURLToBase64 (dataURL) {
|
||||||
|
var BASE64_MARKER = ';base64,';
|
||||||
|
if (dataURL.indexOf(BASE64_MARKER) == -1) {
|
||||||
|
var parts = dataURL.split(',');
|
||||||
|
var contentType = parts[0].split(':')[1];
|
||||||
|
var raw = parts[1];
|
||||||
|
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = dataURL.split(BASE64_MARKER);
|
||||||
|
var contentType = parts[0].split(':')[1];
|
||||||
|
var raw = window.atob(parts[1]);
|
||||||
|
var rawLength = raw.length;
|
||||||
|
|
||||||
|
var uInt8Array = new Uint8Array(rawLength);
|
||||||
|
|
||||||
|
for (var i = 0; i < rawLength; ++i) {
|
||||||
|
uInt8Array[i] = raw.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return uInt8Array;
|
||||||
|
}
|
||||||
1
17th-web/src/assets/logo.svg
Normal file
1
17th-web/src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||||
|
After Width: | Height: | Size: 276 B |
52
17th-web/src/components/CoursesList.vue
Normal file
52
17th-web/src/components/CoursesList.vue
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div class="panel panel-primary">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h2>Courses</h2>
|
||||||
|
</div>
|
||||||
|
<div id="courses" class="panel-body">
|
||||||
|
<div class="list-group">
|
||||||
|
<div class="list-group-item" v-for="course in courses" :key="course">
|
||||||
|
<h4 class="list-group-item-heading">{{ course.name }}</h4>
|
||||||
|
<div
|
||||||
|
class="list-group-item-text"
|
||||||
|
style="display: flex; flex-direction: row; justify-content: space-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p>{{ course.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
mounted() {
|
||||||
|
// Get the courses from the server
|
||||||
|
fetch('http://localhost:3001/api/courses')
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
this.courses = data
|
||||||
|
})
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
courses: [
|
||||||
|
{
|
||||||
|
name: 'Course 1',
|
||||||
|
description: 'This is a course'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Course 2',
|
||||||
|
description: 'This is a course'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
43
17th-web/src/components/HelloWorld.vue
Normal file
43
17th-web/src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
msg: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="greetings">
|
||||||
|
<h1 class="green">{{ msg }}</h1>
|
||||||
|
<h3>
|
||||||
|
You’ve successfully created a project with
|
||||||
|
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
||||||
|
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
h1 {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 2.6rem;
|
||||||
|
top: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.greetings h1,
|
||||||
|
.greetings h3 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.greetings h1,
|
||||||
|
.greetings h3 {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
65
17th-web/src/components/NavBar.vue
Normal file
65
17th-web/src/components/NavBar.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<v-app-bar color="teal-darken-4" image="https://picsum.photos/1920/1080?random" prominent>
|
||||||
|
<template v-slot:image>
|
||||||
|
<v-img gradient="to top right, rgba(19,84,122,.8), rgba(128,208,199,.8)"></v-img>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-app-bar-nav-icon variant="text" @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-app-bar-title>Title</v-app-bar-title>
|
||||||
|
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<router-link to="/courses" custom v-slot="{ navigate }">
|
||||||
|
<v-btn icon="mdi-school-outline" @click="navigate"></v-btn>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<router-link to="/ribbons" custom v-slot="{ navigate }">
|
||||||
|
<v-btn icon="mdi-seal-variant" @click="navigate"></v-btn>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<v-btn icon="mdi-dots-vertical"> </v-btn>
|
||||||
|
</v-app-bar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'NavBar',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
navigation: [
|
||||||
|
{ name: 'Home', href: '/', current: this.$route.path.includes('home') },
|
||||||
|
{ name: 'Ribbons', href: '/ribbons', current: this.$route.path.includes('ribbons') },
|
||||||
|
{ name: 'Courses', href: '/courses', current: this.$route.path.includes('courses') }
|
||||||
|
],
|
||||||
|
drawer: true,
|
||||||
|
items: [
|
||||||
|
{ title: 'Home', icon: 'mdi-home-city' },
|
||||||
|
{ title: 'My Account', icon: 'mdi-account' },
|
||||||
|
{ title: 'Users', icon: 'mdi-account-group-outline' }
|
||||||
|
],
|
||||||
|
rail: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route() {
|
||||||
|
this.navigation = [
|
||||||
|
{ name: 'Home', href: '/', current: this.$route.path.includes('home') },
|
||||||
|
{ name: 'Ribbons', href: '/ribbons', current: this.$route.path.includes('ribbons') },
|
||||||
|
{ name: 'Courses', href: '/courses', current: this.$route.path.includes('courses') }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#navbar {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
86
17th-web/src/components/TheWelcome.vue
Normal file
86
17th-web/src/components/TheWelcome.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<script setup>
|
||||||
|
import WelcomeItem from './WelcomeItem.vue'
|
||||||
|
import DocumentationIcon from './icons/IconDocumentation.vue'
|
||||||
|
import ToolingIcon from './icons/IconTooling.vue'
|
||||||
|
import EcosystemIcon from './icons/IconEcosystem.vue'
|
||||||
|
import CommunityIcon from './icons/IconCommunity.vue'
|
||||||
|
import SupportIcon from './icons/IconSupport.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<DocumentationIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Documentation</template>
|
||||||
|
|
||||||
|
Vue’s
|
||||||
|
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
||||||
|
provides you with all information you need to get started.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<ToolingIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Tooling</template>
|
||||||
|
|
||||||
|
This project is served and bundled with
|
||||||
|
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
||||||
|
recommended IDE setup is
|
||||||
|
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
||||||
|
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
||||||
|
you need to test your components and web pages, check out
|
||||||
|
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
||||||
|
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
More instructions are available in <code>README.md</code>.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<EcosystemIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Ecosystem</template>
|
||||||
|
|
||||||
|
Get official tools and libraries for your project:
|
||||||
|
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
||||||
|
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
||||||
|
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
||||||
|
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
||||||
|
you need more resources, we suggest paying
|
||||||
|
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
||||||
|
a visit.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<CommunityIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Community</template>
|
||||||
|
|
||||||
|
Got stuck? Ask your question on
|
||||||
|
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
||||||
|
Discord server, or
|
||||||
|
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
||||||
|
>StackOverflow</a
|
||||||
|
>. You should also subscribe to
|
||||||
|
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
||||||
|
the official
|
||||||
|
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
||||||
|
twitter account for latest news in the Vue world.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<SupportIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Support Vue</template>
|
||||||
|
|
||||||
|
As an independent project, Vue relies on community backing for its sustainability. You can help
|
||||||
|
us by
|
||||||
|
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
||||||
|
</WelcomeItem>
|
||||||
|
</template>
|
||||||
85
17th-web/src/components/WelcomeItem.vue
Normal file
85
17th-web/src/components/WelcomeItem.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div class="item">
|
||||||
|
<i>
|
||||||
|
<slot name="icon"></slot>
|
||||||
|
</i>
|
||||||
|
<div class="details">
|
||||||
|
<h3>
|
||||||
|
<slot name="heading"></slot>
|
||||||
|
</h3>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.item {
|
||||||
|
margin-top: 2rem;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
place-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
color: var(--color-heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.item {
|
||||||
|
margin-top: 0;
|
||||||
|
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
top: calc(50% - 25px);
|
||||||
|
left: -26px;
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
background: var(--color-background);
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:before {
|
||||||
|
content: ' ';
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: calc(50% + 25px);
|
||||||
|
height: calc(50% - 25px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:after {
|
||||||
|
content: ' ';
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: calc(50% + 25px);
|
||||||
|
height: calc(50% - 25px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:first-of-type:before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:last-of-type:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
7
17th-web/src/components/icons/IconCommunity.vue
Normal file
7
17th-web/src/components/icons/IconCommunity.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
17th-web/src/components/icons/IconDocumentation.vue
Normal file
7
17th-web/src/components/icons/IconDocumentation.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
17th-web/src/components/icons/IconEcosystem.vue
Normal file
7
17th-web/src/components/icons/IconEcosystem.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
17th-web/src/components/icons/IconSupport.vue
Normal file
7
17th-web/src/components/icons/IconSupport.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
19
17th-web/src/components/icons/IconTooling.vue
Normal file
19
17th-web/src/components/icons/IconTooling.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
aria-hidden="true"
|
||||||
|
role="img"
|
||||||
|
class="iconify iconify--mdi"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
8
17th-web/src/config.js
Normal file
8
17th-web/src/config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const httpConfig = axios.create({
|
||||||
|
baseURL: "http://localhost:3001/api",
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json"
|
||||||
|
}
|
||||||
|
})
|
||||||
17
17th-web/src/main.js
Normal file
17
17th-web/src/main.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
import App from '@/App.vue'
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
|
import '@/style.css';
|
||||||
|
|
||||||
|
// Vuetify
|
||||||
|
import vuetify from '@/plugins/vuetify'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(vuetify)
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
16
17th-web/src/plugins/vuetify.js
Normal file
16
17th-web/src/plugins/vuetify.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import '@mdi/font/css/materialdesignicons.css' // Ensure you are using css-loader
|
||||||
|
import { createVuetify } from 'vuetify'
|
||||||
|
import 'vuetify/styles'
|
||||||
|
import * as components from 'vuetify/components'
|
||||||
|
import * as directives from 'vuetify/directives'
|
||||||
|
|
||||||
|
import { md2 } from 'vuetify/blueprints'
|
||||||
|
|
||||||
|
export default createVuetify({
|
||||||
|
blueprint: md2,
|
||||||
|
components,
|
||||||
|
directives,
|
||||||
|
icons: {
|
||||||
|
defaultSet: 'mdi', // This is already the default value - only for display purposes
|
||||||
|
},
|
||||||
|
})
|
||||||
17
17th-web/src/router/dbRoutes/Course.js
Normal file
17
17th-web/src/router/dbRoutes/Course.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: '/courses',
|
||||||
|
name: 'courses',
|
||||||
|
component: () => import('@/views/dbViews/CoursesView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/courses/:id',
|
||||||
|
name: 'course',
|
||||||
|
component: () => import('@/views/dbViews/CourseView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/courses/add',
|
||||||
|
name: 'add-course',
|
||||||
|
component: () => import('@/views/dbViews/AddCourseView.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
17
17th-web/src/router/dbRoutes/QualificationCategory.js
Normal file
17
17th-web/src/router/dbRoutes/QualificationCategory.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: '/qualification-categories',
|
||||||
|
name: 'qualification-categories',
|
||||||
|
component: () => import('@/views/dbViews/QualificationCategoriesView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/qualification-categories/:id',
|
||||||
|
name: 'qualification-category',
|
||||||
|
component: () => import('@/views/dbViews/QualificationCategoryView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/qualification-categories/add',
|
||||||
|
name: 'add-qualification-category',
|
||||||
|
component: () => import('@/views/dbViews/AddQualificationCategoryView.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
17
17th-web/src/router/dbRoutes/Ribbon.js
Normal file
17
17th-web/src/router/dbRoutes/Ribbon.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: '/ribbons',
|
||||||
|
name: 'ribbons',
|
||||||
|
component: () => import('@/views/dbViews/RibbonsView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ribbons/:id',
|
||||||
|
name: 'ribbon',
|
||||||
|
component: () => import('@/views/dbViews/RibbonView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ribbons/add',
|
||||||
|
name: 'add-ribbon',
|
||||||
|
component: () => import('@/views/dbViews/AddRibbonView.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
38
17th-web/src/router/index.js
Normal file
38
17th-web/src/router/index.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
import RibbonRoutes from './dbRoutes/Ribbon.js'
|
||||||
|
import CourseRoutes from './dbRoutes/Course.js'
|
||||||
|
import QualificationCategoryRoutes from './dbRoutes/QualificationCategory.js'
|
||||||
|
|
||||||
|
const routes =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: () => import('@/views/HomeView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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('../views/AboutView.vue')
|
||||||
|
},
|
||||||
|
...RibbonRoutes,
|
||||||
|
...CourseRoutes,
|
||||||
|
...QualificationCategoryRoutes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
33
17th-web/src/services/CourseDataService.js
Normal file
33
17th-web/src/services/CourseDataService.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { httpConfig } from "@/config.js";
|
||||||
|
|
||||||
|
class CourseDataService {
|
||||||
|
getAll () {
|
||||||
|
return httpConfig.get("/courses");
|
||||||
|
}
|
||||||
|
|
||||||
|
get (id) {
|
||||||
|
return httpConfig.get(`/courses/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
create (data) {
|
||||||
|
return httpConfig.post("/courses", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
update (id, data) {
|
||||||
|
return httpConfig.put(`/courses/${id}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete (id) {
|
||||||
|
return httpConfig.delete(`/courses/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAll () {
|
||||||
|
return httpConfig.delete(`/courses`);
|
||||||
|
}
|
||||||
|
|
||||||
|
findByName (name) {
|
||||||
|
return httpConfig.get(`/courses?name=${name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new CourseDataService();
|
||||||
33
17th-web/src/services/QualificationCategoryDataService.js
Normal file
33
17th-web/src/services/QualificationCategoryDataService.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { httpConfig } from "@/config.js";
|
||||||
|
|
||||||
|
class QualificationCategoryDataService {
|
||||||
|
getAll () {
|
||||||
|
return httpConfig.get("/qualification-categories");
|
||||||
|
}
|
||||||
|
|
||||||
|
get (id) {
|
||||||
|
return httpConfig.get(`/qualification-categories/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
create (data) {
|
||||||
|
return httpConfig.post("/qualification-categories", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
update (id, data) {
|
||||||
|
return httpConfig.put(`/qualification-categories/${id}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete (id) {
|
||||||
|
return httpConfig.delete(`/qualification-categories/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAll () {
|
||||||
|
return httpConfig.delete(`/qualification-categories`);
|
||||||
|
}
|
||||||
|
|
||||||
|
findByName (name) {
|
||||||
|
return httpConfig.get(`/qualification-categories?name=${name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new QualificationCategoryDataService();
|
||||||
39
17th-web/src/services/RibbonDataService.js
Normal file
39
17th-web/src/services/RibbonDataService.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { httpConfig } from "@/config.js";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
class RibbonDataService {
|
||||||
|
getAll () {
|
||||||
|
return httpConfig.get("/ribbons");
|
||||||
|
}
|
||||||
|
|
||||||
|
get (id) {
|
||||||
|
return httpConfig.get(`/ribbons/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
create (data) {
|
||||||
|
return httpConfig.post("/ribbons", data);
|
||||||
|
// return axios.post("https://httpbin.org/post", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
update (id, data) {
|
||||||
|
return httpConfig.put(`/ribbons/${id}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete (id) {
|
||||||
|
return httpConfig.delete(`/ribbons/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAll () {
|
||||||
|
return httpConfig.delete(`/ribbons`);
|
||||||
|
}
|
||||||
|
|
||||||
|
findByName (name) {
|
||||||
|
return httpConfig.get(`/ribbons?name=${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCategories () {
|
||||||
|
return httpConfig.get("/qualification-categories");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new RibbonDataService();
|
||||||
12
17th-web/src/stores/counter.js
Normal file
12
17th-web/src/stores/counter.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useCounterStore = defineStore('counter', () => {
|
||||||
|
const count = ref(0)
|
||||||
|
const doubleCount = computed(() => count.value * 2)
|
||||||
|
function increment() {
|
||||||
|
count.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
return { count, doubleCount, increment }
|
||||||
|
})
|
||||||
7
17th-web/src/style.css
Normal file
7
17th-web/src/style.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#app {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
15
17th-web/src/views/AboutView.vue
Normal file
15
17th-web/src/views/AboutView.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<div class="about">
|
||||||
|
<h1>This is an about page</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.about {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
9
17th-web/src/views/HomeView.vue
Normal file
9
17th-web/src/views/HomeView.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<script setup>
|
||||||
|
import TheWelcome from '../components/TheWelcome.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<TheWelcome />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
121
17th-web/src/views/dbViews/AddCourseView.vue
Normal file
121
17th-web/src/views/dbViews/AddCourseView.vue
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<template>
|
||||||
|
<div class="submit-form">
|
||||||
|
<div v-if="!submitted">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="name"
|
||||||
|
required
|
||||||
|
v-model="course.name"
|
||||||
|
name="name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="shortname">Short Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="shortname"
|
||||||
|
required
|
||||||
|
v-model="course.shortname"
|
||||||
|
name="shortname"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
id="description"
|
||||||
|
required
|
||||||
|
v-model="course.description"
|
||||||
|
name="description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- allow image upload for blob -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="image">Image</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="form-control"
|
||||||
|
id="image"
|
||||||
|
required
|
||||||
|
name="image"
|
||||||
|
@change="onImageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="saveCourse" class="btn btn-success">Submit</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<h4>You submitted successfully!</h4>
|
||||||
|
<button class="btn btn-success" @click="newCourse">Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CourseDataService from '@/services/CourseDataService'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'add-course',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
course: {
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
shortname: '',
|
||||||
|
description: '',
|
||||||
|
image: new Blob(),
|
||||||
|
category: '',
|
||||||
|
footprint: ''
|
||||||
|
},
|
||||||
|
submitted: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
saveCourse() {
|
||||||
|
var data = {
|
||||||
|
name: this.course.name,
|
||||||
|
shortname: this.course.shortname,
|
||||||
|
description: this.course.description,
|
||||||
|
image: this.course.image ? this.course.image : null,
|
||||||
|
category: this.course.category,
|
||||||
|
footprint: this.course.footprint
|
||||||
|
}
|
||||||
|
|
||||||
|
CourseDataService.create(data)
|
||||||
|
.then((response) => {
|
||||||
|
this.course.id = response.data.id
|
||||||
|
console.log(response.data)
|
||||||
|
this.submitted = true
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
newCourse() {
|
||||||
|
this.submitted = false
|
||||||
|
this.course = {}
|
||||||
|
},
|
||||||
|
|
||||||
|
onImageChange(e) {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
this.course.image = new Blob([file])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.submit-form {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
70
17th-web/src/views/dbViews/AddQualificationCategoryView.vue
Normal file
70
17th-web/src/views/dbViews/AddQualificationCategoryView.vue
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<div class="submit-form">
|
||||||
|
<div v-if="!submitted">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="name"
|
||||||
|
required
|
||||||
|
v-model="qualificationCategory.name"
|
||||||
|
name="name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="saveQualificationCategory" class="btn btn-success">Submit</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<h4>You submitted successfully!</h4>
|
||||||
|
<button class="btn btn-success" @click="newQualificationCategory">Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import QualificationCategoryDataService from '@/services/QualificationCategoryDataService'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'add-qualification-category',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
qualificationCategory: {
|
||||||
|
id: null,
|
||||||
|
name: ''
|
||||||
|
},
|
||||||
|
submitted: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
saveQualificationCategory() {
|
||||||
|
var data = {
|
||||||
|
name: this.qualificationCategory.name
|
||||||
|
}
|
||||||
|
|
||||||
|
QualificationCategoryDataService.create(data)
|
||||||
|
.then((response) => {
|
||||||
|
this.qualificationCategory.id = response.data.id
|
||||||
|
console.log(response.data)
|
||||||
|
this.submitted = true
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
newQualificationCategory() {
|
||||||
|
this.submitted = false
|
||||||
|
this.qualificationCategory = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.submit-form {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
170
17th-web/src/views/dbViews/AddRibbonView.vue
Normal file
170
17th-web/src/views/dbViews/AddRibbonView.vue
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<template>
|
||||||
|
<div class="submit-form">
|
||||||
|
<div v-if="!submitted">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="name"
|
||||||
|
required
|
||||||
|
v-model="ribbon.name"
|
||||||
|
name="name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="shortname">Short Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="shortname"
|
||||||
|
required
|
||||||
|
v-model="ribbon.shortname"
|
||||||
|
name="shortname"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
id="description"
|
||||||
|
required
|
||||||
|
v-model="ribbon.description"
|
||||||
|
name="description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- allow image upload for blob -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="image">Image</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="form-control"
|
||||||
|
id="image"
|
||||||
|
required
|
||||||
|
name="image"
|
||||||
|
@change="onImageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- footprint -->
|
||||||
|
<!-- front-end validation only -->
|
||||||
|
<!-- select between badge and ribbon type -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="footprint">Footprint</label>
|
||||||
|
<select
|
||||||
|
class="form-control"
|
||||||
|
id="footprint"
|
||||||
|
required
|
||||||
|
v-model="ribbon.footprint"
|
||||||
|
name="footprint"
|
||||||
|
>
|
||||||
|
<option value="badge">Badge</option>
|
||||||
|
<option value="ribbon">Ribbon</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- select list of categories -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category">Category</label>
|
||||||
|
<select
|
||||||
|
class="form-control"
|
||||||
|
id="category"
|
||||||
|
required
|
||||||
|
v-model="ribbon.category"
|
||||||
|
name="category"
|
||||||
|
@click="getCategories"
|
||||||
|
>
|
||||||
|
<option v-for="category in categories" :key="category.id" :value="category.id">
|
||||||
|
{{ category.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="saveRibbon" class="btn btn-success">Submit</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<h4>You submitted successfully!</h4>
|
||||||
|
<button class="btn btn-success" @click="newRibbon">Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import RibbonDataService from '@/services/RibbonDataService'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'add-ribbon',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
ribbon: {
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
shortname: '',
|
||||||
|
description: '',
|
||||||
|
image: null,
|
||||||
|
category: '',
|
||||||
|
footprint: ''
|
||||||
|
},
|
||||||
|
categories: [],
|
||||||
|
submitted: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getCategories()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
saveRibbon() {
|
||||||
|
var data = {
|
||||||
|
name: this.ribbon.name,
|
||||||
|
shortname: this.ribbon.shortname,
|
||||||
|
description: this.ribbon.description,
|
||||||
|
image: this.ribbon.image ? this.ribbon.image : null,
|
||||||
|
category: this.ribbon.category,
|
||||||
|
footprint: this.ribbon.footprint
|
||||||
|
}
|
||||||
|
|
||||||
|
RibbonDataService.create(data)
|
||||||
|
.then((response) => {
|
||||||
|
this.ribbon.id = response.data.id
|
||||||
|
console.log(response.data)
|
||||||
|
this.submitted = true
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
newRibbon() {
|
||||||
|
this.submitted = false
|
||||||
|
this.ribbon = {}
|
||||||
|
},
|
||||||
|
|
||||||
|
getCategories() {
|
||||||
|
RibbonDataService.getCategories()
|
||||||
|
.then((response) => {
|
||||||
|
this.categories = response.data
|
||||||
|
console.log(response.data)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onImageChange(e) {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
this.ribbon.image = new Blob([file])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.submit-form {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
122
17th-web/src/views/dbViews/CourseView.vue
Normal file
122
17th-web/src/views/dbViews/CourseView.vue
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="currentCourse" class="edit-form">
|
||||||
|
<h4>Course</h4>
|
||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input type="text" class="form-control" id="name" v-model="currentCourse.name" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="description"
|
||||||
|
v-model="currentCourse.description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label><strong>Status:</strong></label>
|
||||||
|
{{ currentCourse.published ? 'Published' : 'Pending' }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="badge badge-primary mr-2"
|
||||||
|
v-if="currentCourse.published"
|
||||||
|
@click="updatePublished(false)"
|
||||||
|
>
|
||||||
|
UnPublish
|
||||||
|
</button>
|
||||||
|
<button v-else class="badge badge-primary mr-2" @click="updatePublished(true)">Publish</button>
|
||||||
|
|
||||||
|
<button class="badge badge-danger mr-2" @click="deleteCourse">Delete</button>
|
||||||
|
|
||||||
|
<button type="submit" class="badge badge-success" @click="updateCourse">Update</button>
|
||||||
|
<p>{{ message }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<br />
|
||||||
|
<p>Please click on a Course...</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CourseDataService from '@/services/CourseDataService'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'course',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentCourse: null,
|
||||||
|
message: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getCourse(id) {
|
||||||
|
CourseDataService.get(id)
|
||||||
|
.then((response) => {
|
||||||
|
this.currentCourse = response.data
|
||||||
|
console.log(response.data)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePublished(status) {
|
||||||
|
var data = {
|
||||||
|
id: this.currentCourse.id,
|
||||||
|
name: this.currentCourse.name,
|
||||||
|
description: this.currentCourse.description,
|
||||||
|
published: status
|
||||||
|
}
|
||||||
|
|
||||||
|
CourseDataService.update(this.currentCourse.id, data)
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response.data)
|
||||||
|
this.currentCourse.published = status
|
||||||
|
this.message = 'The status was updated successfully!'
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCourse() {
|
||||||
|
CourseDataService.update(this.currentCourse.id, this.currentCourse)
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response.data)
|
||||||
|
this.message = 'The course was updated successfully!'
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteCourse() {
|
||||||
|
CourseDataService.delete(this.currentCourse.id)
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response.data)
|
||||||
|
this.$router.push({ name: 'courses' })
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.message = ''
|
||||||
|
this.getCourse(this.$route.params.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.edit-form {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
126
17th-web/src/views/dbViews/CoursesView.vue
Normal file
126
17th-web/src/views/dbViews/CoursesView.vue
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<div class="list row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text" class="form-control" placeholder="Search by name" v-model="name" />
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" @click="searchName">
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h4>Courses List</h4>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li
|
||||||
|
class="list-group-item"
|
||||||
|
:class="{ active: index == currentIndex }"
|
||||||
|
v-for="(course, index) in courses"
|
||||||
|
:key="index"
|
||||||
|
@click="setActiveCourse(course, index)"
|
||||||
|
>
|
||||||
|
{{ course.name }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button class="m-3 btn btn-sm btn-danger" @click="removeAllCourses">Remove All</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div v-if="currentCourse">
|
||||||
|
<h4>Course</h4>
|
||||||
|
<div>
|
||||||
|
<label><strong>Name:</strong></label> {{ currentCourse.name }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label><strong>Description:</strong></label> {{ currentCourse.description }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label><strong>Status:</strong></label>
|
||||||
|
{{ currentCourse.published ? 'Published' : 'Pending' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<router-link :to="'/courses/' + currentCourse.id" class="badge badge-warning"
|
||||||
|
>Edit</router-link
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<br />
|
||||||
|
<p>Please click on a Course...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CourseDataService from '@/services/CourseDataService'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'courses-list',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
courses: [],
|
||||||
|
currentCourse: null,
|
||||||
|
currentIndex: -1,
|
||||||
|
name: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
retrieveCourses() {
|
||||||
|
CourseDataService.getAll()
|
||||||
|
.then((response) => {
|
||||||
|
this.courses = response.data
|
||||||
|
console.log(response.data)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshList() {
|
||||||
|
this.retrieveCourses()
|
||||||
|
this.currentCourse = null
|
||||||
|
this.currentIndex = -1
|
||||||
|
},
|
||||||
|
|
||||||
|
setActiveCourse(course, index) {
|
||||||
|
this.currentCourse = course
|
||||||
|
this.currentIndex = course ? index : -1
|
||||||
|
},
|
||||||
|
|
||||||
|
removeAllCourses() {
|
||||||
|
CourseDataService.deleteAll()
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response.data)
|
||||||
|
this.refreshList()
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
searchName() {
|
||||||
|
CourseDataService.findByName(this.name)
|
||||||
|
.then((response) => {
|
||||||
|
this.courses = response.data
|
||||||
|
this.setActiveCourse(null)
|
||||||
|
console.log(response.data)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.retrieveCourses()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.list {
|
||||||
|
text-align: left;
|
||||||
|
max-width: 750px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
126
17th-web/src/views/dbViews/QualificationCategoriesView.vue
Normal file
126
17th-web/src/views/dbViews/QualificationCategoriesView.vue
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<div class="list row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text" class="form-control" placeholder="Search by name" v-model="name" />
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" @click="searchName">
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h4>Courses List</h4>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li
|
||||||
|
class="list-group-item"
|
||||||
|
:class="{ active: index == currentIndex }"
|
||||||
|
v-for="(course, index) in courses"
|
||||||
|
:key="index"
|
||||||
|
@click="setActiveCourse(course, index)"
|
||||||
|
>
|
||||||
|
{{ course.name }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button class="m-3 btn btn-sm btn-danger" @click="removeAllCourses">Remove All</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div v-if="currentCourse">
|
||||||
|
<h4>Course</h4>
|
||||||
|
<div>
|
||||||
|
<label><strong>Name:</strong></label> {{ currentCourse.name }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label><strong>Description:</strong></label> {{ currentCourse.description }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label><strong>Status:</strong></label>
|
||||||
|
{{ currentCourse.published ? 'Published' : 'Pending' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<router-link :to="'/courses/' + currentCourse.id" class="badge badge-warning"
|
||||||
|
>Edit</router-link
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<br />
|
||||||
|
<p>Please click on a Course...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CourseDataService from '@/services/CourseDataService'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'courses-list',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
courses: [],
|
||||||
|
currentCourse: null,
|
||||||
|
currentIndex: -1,
|
||||||
|
name: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
retrieveCourses() {
|
||||||
|
CourseDataService.getAll()
|
||||||
|
.then((response) => {
|
||||||
|
this.courses = response.data
|
||||||
|
console.log(response.data)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshList() {
|
||||||
|
this.retrieveCourses()
|
||||||
|
this.currentCourse = null
|
||||||
|
this.currentIndex = -1
|
||||||
|
},
|
||||||
|
|
||||||
|
setActiveCourse(course, index) {
|
||||||
|
this.currentCourse = course
|
||||||
|
this.currentIndex = course ? index : -1
|
||||||
|
},
|
||||||
|
|
||||||
|
removeAllCourses() {
|
||||||
|
CourseDataService.deleteAll()
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response.data)
|
||||||
|
this.refreshList()
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
searchName() {
|
||||||
|
CourseDataService.findByName(this.name)
|
||||||
|
.then((response) => {
|
||||||
|
this.courses = response.data
|
||||||
|
this.setActiveCourse(null)
|
||||||
|
console.log(response.data)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.retrieveCourses()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.list {
|
||||||
|
text-align: left;
|
||||||
|
max-width: 750px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
122
17th-web/src/views/dbViews/QualificationCategoryView.vue
Normal file
122
17th-web/src/views/dbViews/QualificationCategoryView.vue
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="currentCourse" class="edit-form">
|
||||||
|
<h4>Course</h4>
|
||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input type="text" class="form-control" id="name" v-model="currentCourse.name" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="description"
|
||||||
|
v-model="currentCourse.description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label><strong>Status:</strong></label>
|
||||||
|
{{ currentCourse.published ? 'Published' : 'Pending' }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="badge badge-primary mr-2"
|
||||||
|
v-if="currentCourse.published"
|
||||||
|
@click="updatePublished(false)"
|
||||||
|
>
|
||||||
|
UnPublish
|
||||||
|
</button>
|
||||||
|
<button v-else class="badge badge-primary mr-2" @click="updatePublished(true)">Publish</button>
|
||||||
|
|
||||||
|
<button class="badge badge-danger mr-2" @click="deleteCourse">Delete</button>
|
||||||
|
|
||||||
|
<button type="submit" class="badge badge-success" @click="updateCourse">Update</button>
|
||||||
|
<p>{{ message }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<br />
|
||||||
|
<p>Please click on a Course...</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CourseDataService from '@/services/CourseDataService'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'course',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentCourse: null,
|
||||||
|
message: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getCourse(id) {
|
||||||
|
CourseDataService.get(id)
|
||||||
|
.then((response) => {
|
||||||
|
this.currentCourse = response.data
|
||||||
|
console.log(response.data)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePublished(status) {
|
||||||
|
var data = {
|
||||||
|
id: this.currentCourse.id,
|
||||||
|
name: this.currentCourse.name,
|
||||||
|
description: this.currentCourse.description,
|
||||||
|
published: status
|
||||||
|
}
|
||||||
|
|
||||||
|
CourseDataService.update(this.currentCourse.id, data)
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response.data)
|
||||||
|
this.currentCourse.published = status
|
||||||
|
this.message = 'The status was updated successfully!'
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCourse() {
|
||||||
|
CourseDataService.update(this.currentCourse.id, this.currentCourse)
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response.data)
|
||||||
|
this.message = 'The course was updated successfully!'
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteCourse() {
|
||||||
|
CourseDataService.delete(this.currentCourse.id)
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response.data)
|
||||||
|
this.$router.push({ name: 'courses' })
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.message = ''
|
||||||
|
this.getCourse(this.$route.params.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.edit-form {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
197
17th-web/src/views/dbViews/RibbonView.vue
Normal file
197
17th-web/src/views/dbViews/RibbonView.vue
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-10">
|
||||||
|
<v-form class="mt-5 md:col-span-2 md:mt-0" v-if="currentRibbon">
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field v-model="currentRibbon.name" label="Ribbon Name" outlined></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
v-model="currentRibbon.shortname"
|
||||||
|
label="Short Name"
|
||||||
|
outlined
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-combobox
|
||||||
|
label="Category"
|
||||||
|
v-model="selectedCategory"
|
||||||
|
:items="categories.map((category) => category.name)"
|
||||||
|
item-text="name"
|
||||||
|
>
|
||||||
|
</v-combobox>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-combobox
|
||||||
|
v-model="currentRibbon.footprint"
|
||||||
|
:items="['ribbon', 'badge']"
|
||||||
|
label="Footprint"
|
||||||
|
></v-combobox>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-textarea
|
||||||
|
v-model="currentRibbon.description"
|
||||||
|
label="Description"
|
||||||
|
counter="500"
|
||||||
|
variant="outlined"
|
||||||
|
></v-textarea>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-img
|
||||||
|
v-if="displayedImage"
|
||||||
|
:src="displayedImage"
|
||||||
|
max-width="200"
|
||||||
|
max-height="200"
|
||||||
|
></v-img>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-file-input
|
||||||
|
label="Image"
|
||||||
|
prepend-icon="mdi-camera"
|
||||||
|
accept="image/*"
|
||||||
|
@change="onFileChange"
|
||||||
|
outlined
|
||||||
|
></v-file-input>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-btn-group variant="flat">
|
||||||
|
<v-btn color="error" @click="deleteRibbon">Delete</v-btn>
|
||||||
|
<v-btn color="info" @click="revertRibbon">Reset</v-btn>
|
||||||
|
<v-btn color="success" @click="updateRibbon">Save</v-btn>
|
||||||
|
</v-btn-group>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import RibbonDataService from '@/services/RibbonDataService'
|
||||||
|
import { bufferToBase64 } from '@/assets/js/imageUtils.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ribbon',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentRibbon: null,
|
||||||
|
message: '',
|
||||||
|
categories: [],
|
||||||
|
selectedCategory: '',
|
||||||
|
displayedImage: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getRibbon(id) {
|
||||||
|
RibbonDataService.get(id)
|
||||||
|
.then((response) => {
|
||||||
|
this.currentRibbon = response.data
|
||||||
|
this.selectedCategory = this.currentRibbon.category?.name
|
||||||
|
|
||||||
|
if (this.currentRibbon.image) {
|
||||||
|
console.log(this.currentRibbon.image)
|
||||||
|
bufferToBase64(this.currentRibbon.image.data, (base64) => {
|
||||||
|
this.displayedImage = 'data:image/png;base64,' + base64
|
||||||
|
this.currentRibbon.image = base64
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(this.currentRibbon)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePublished(status) {
|
||||||
|
var data = {
|
||||||
|
id: this.currentRibbon.id,
|
||||||
|
name: this.currentRibbon.name,
|
||||||
|
description: this.currentRibbon.description,
|
||||||
|
published: status
|
||||||
|
}
|
||||||
|
|
||||||
|
RibbonDataService.update(this.currentRibbon.id, data)
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response.data)
|
||||||
|
this.currentRibbon.published = status
|
||||||
|
this.message = 'The status was updated successfully!'
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateRibbon() {
|
||||||
|
RibbonDataService.update(this.currentRibbon.id, this.currentRibbon)
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response.data)
|
||||||
|
this.message = 'The ribbon was updated successfully!'
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteRibbon() {
|
||||||
|
RibbonDataService.delete(this.currentRibbon.id)
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response.data)
|
||||||
|
this.$router.push({ name: 'ribbons' })
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
revertRibbon() {
|
||||||
|
this.currentRibbon = null
|
||||||
|
this.displayedImage = null
|
||||||
|
this.getRibbon(this.$route.params.id)
|
||||||
|
},
|
||||||
|
|
||||||
|
getCategories() {
|
||||||
|
RibbonDataService.getCategories()
|
||||||
|
.then((response) => {
|
||||||
|
this.categories = response.data
|
||||||
|
console.log(response.data)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onFileChange(e) {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
// convert file to dataUrl
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
reader.onload = () => {
|
||||||
|
this.displayedImage = reader.result
|
||||||
|
// convert data to Blob then buffer
|
||||||
|
const dataUrl = reader.result
|
||||||
|
const base64 = dataUrl.split(',')[1]
|
||||||
|
const buffer = new Uint8Array(
|
||||||
|
atob(base64)
|
||||||
|
.split('')
|
||||||
|
.map((char) => char.charCodeAt(0))
|
||||||
|
)
|
||||||
|
this.currentRibbon.image = buffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.message = ''
|
||||||
|
this.getCategories()
|
||||||
|
this.getRibbon(this.$route.params.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.edit-form {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
226
17th-web/src/views/dbViews/RibbonsView.vue
Normal file
226
17th-web/src/views/dbViews/RibbonsView.vue
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<v-card>
|
||||||
|
<v-toolbar color="primary">
|
||||||
|
<v-toolbar-title> Search and Filter </v-toolbar-title>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-row class="px-3 justify-center" align="center">
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-form>
|
||||||
|
<v-row class="px-3">
|
||||||
|
<v-text-field v-model="searchInputName" label="Name" outlined />
|
||||||
|
<v-btn @click="searchByName" icon="mdi-magnify"></v-btn>
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-combobox
|
||||||
|
v-model="searchSelectionsCategories"
|
||||||
|
:items="searchCategories"
|
||||||
|
label="Categories"
|
||||||
|
multiple
|
||||||
|
chips
|
||||||
|
></v-combobox>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-combobox
|
||||||
|
v-model="searchSelectionsFootprints"
|
||||||
|
:items="searchFootprints"
|
||||||
|
label="Footprint"
|
||||||
|
multiple
|
||||||
|
chips
|
||||||
|
></v-combobox>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-btn to="/ribbons/add" color="green-darken-4"> Add Ribbon </v-btn>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-card
|
||||||
|
v-for="(ribbon, index) in ribbonsFiltered"
|
||||||
|
:key="index"
|
||||||
|
class="mt-5 mb-8"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
<v-row justify="space-between">
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-card-title>
|
||||||
|
{{ ribbon.name }}
|
||||||
|
{{ ribbon.footprint.charAt(0).toUpperCase() + ribbon.footprint.slice(1) }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-subtitle>
|
||||||
|
{{ ribbon.shortname }}
|
||||||
|
</v-card-subtitle>
|
||||||
|
<v-card-text>
|
||||||
|
<p>{{ ribbon.description }}</p>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-chip class="ma-1" color="green" variant="flat" size="small">
|
||||||
|
<v-icon start icon="mdi-label"></v-icon>
|
||||||
|
{{ ribbon.footprint }}
|
||||||
|
</v-chip>
|
||||||
|
<v-chip v class="m-1" color="blue" variant="flat" size="small">
|
||||||
|
<v-icon start icon="mdi-label"></v-icon>
|
||||||
|
{{ ribbon.category?.name }}
|
||||||
|
</v-chip>
|
||||||
|
<v-btn :to="'/ribbons/' + ribbon.id" color="secondary" variant="flat" class="mx-2">
|
||||||
|
Edit Ribbon
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="auto" class="m-10">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<v-img
|
||||||
|
v-if="ribbon.image"
|
||||||
|
:src="ribbon.image"
|
||||||
|
height="200"
|
||||||
|
width="200"
|
||||||
|
class="align-middle contain mx-12"
|
||||||
|
></v-img>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import RibbonDataService from '@/services/RibbonDataService.js'
|
||||||
|
import { bufferToBase64 } from '@/assets/js/imageUtils.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ribbons-list',
|
||||||
|
components: {},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
ribbons: [],
|
||||||
|
currentRibbon: null,
|
||||||
|
currentIndex: -1,
|
||||||
|
searchInputName: '',
|
||||||
|
searchCategories: [],
|
||||||
|
searchSelectionsCategories: [],
|
||||||
|
searchFootprints: [],
|
||||||
|
searchSelectionsFootprints: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.retrieveRibbons()
|
||||||
|
},
|
||||||
|
watch: {},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
ribbonsFiltered() {
|
||||||
|
// get unique values from our two query sets
|
||||||
|
if (
|
||||||
|
!this.searchInputName ||
|
||||||
|
!this.searchSelectionsCategories ||
|
||||||
|
!this.searchSelectionsFootprints
|
||||||
|
) {
|
||||||
|
return this.ribbons
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.ribbons.filter((ribbon) => {
|
||||||
|
const nameMatches =
|
||||||
|
this.searchInputName === ''
|
||||||
|
? false
|
||||||
|
: ribbon.name.toLowerCase().includes(this.searchInputName.toLowerCase()) ||
|
||||||
|
ribbon.shortname.toLowerCase().includes(this.searchInputName.toLowerCase())
|
||||||
|
const categoryMatches = this.searchSelectionsCategories.includes(ribbon.category.name)
|
||||||
|
const footprintMatches = this.searchSelectionsFootprints.includes(ribbon.footprint)
|
||||||
|
return nameMatches && categoryMatches && footprintMatches
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
retrieveRibbons() {
|
||||||
|
this.ribbonsFiltered
|
||||||
|
RibbonDataService.getAll()
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response.data)
|
||||||
|
this.ribbons = response.data.map((ribbon) => {
|
||||||
|
return {
|
||||||
|
...ribbon,
|
||||||
|
image: ribbon.image ? this.getImageFromBuffer(ribbon.image) : null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ribbons = this.ribbons.sort((a, b) => (a.name > b.name ? 1 : -1))
|
||||||
|
|
||||||
|
this.searchCategories = [...new Set(this.ribbons.map((ribbon) => ribbon.category.name))]
|
||||||
|
this.searchSelectionsCategories = this.searchCategories
|
||||||
|
this.searchFootprints = [...new Set(this.ribbons.map((ribbon) => ribbon.footprint))]
|
||||||
|
this.searchSelectionsFootprints = this.searchFootprints
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshList() {
|
||||||
|
this.retrieveRibbons()
|
||||||
|
this.currentRibbon = null
|
||||||
|
this.currentIndex = -1
|
||||||
|
},
|
||||||
|
|
||||||
|
setActiveRibbon(ribbon, index) {
|
||||||
|
this.currentRibbon = ribbon
|
||||||
|
this.currentIndex = ribbon ? index : -1
|
||||||
|
},
|
||||||
|
|
||||||
|
removeAllRibbons() {
|
||||||
|
RibbonDataService.deleteAll()
|
||||||
|
.then((response) => {
|
||||||
|
// console.log(response.data)
|
||||||
|
this.refreshList()
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
searchByName() {
|
||||||
|
RibbonDataService.findByName(this.searchInputName)
|
||||||
|
.then((response) => {
|
||||||
|
this.ribbons = response.data
|
||||||
|
this.setActiveRibbon(null)
|
||||||
|
// console.log(response.data)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getImageFromBuffer(blob) {
|
||||||
|
// Convert binary data to base64 encoded string
|
||||||
|
const data = new Uint8Array(blob.data)
|
||||||
|
console.log(data)
|
||||||
|
const blobData = new Blob([data], { type: 'image/png' })
|
||||||
|
console.log(blobData)
|
||||||
|
console.log(URL.createObjectURL(blobData))
|
||||||
|
return URL.createObjectURL(blobData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.list {
|
||||||
|
text-align: left;
|
||||||
|
max-width: 750px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
32
17th-web/tailwind.config.js
Normal file
32
17th-web/tailwind.config.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
const colors = require('tailwindcss/colors')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx,vue}",
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
screens: {
|
||||||
|
sm: '480px',
|
||||||
|
md: '768px',
|
||||||
|
lg: '976px',
|
||||||
|
xl: '1440px',
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'sans-serif'],
|
||||||
|
serif: ['Merriweather', 'serif'],
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
spacing: {
|
||||||
|
'128': '32rem',
|
||||||
|
'144': '36rem',
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
'4xl': '2rem',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
17th-web/vite.config.js
Normal file
16
17th-web/vite.config.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
82
README.md
82
README.md
@@ -1,2 +1,82 @@
|
|||||||
# 17th-Battalion-Tracker
|
# 17th Website Project
|
||||||
|
|
||||||
|
## Basic Dev Environment
|
||||||
|
|
||||||
|
*Assumes you have Docker and Docker Compose installed (included in Docker Desktop).*
|
||||||
|
|
||||||
|
1. Copy `/.env.example` to `/.env` and populate with desired options.
|
||||||
|
1. Navigate to the project root in a terminal.
|
||||||
|
2. Run `docker compose up -d --build db`
|
||||||
|
3. Run `docker compose up -d --build api`
|
||||||
|
4. Let it build the images
|
||||||
|
|
||||||
|
You will have a SQL server accessible on port 12730 from your host (and publicly if not firewalled) for SQL Workbench access.
|
||||||
|
|
||||||
|
The API will launch and be accessible on port 3000 from your host via Postman or a browser.
|
||||||
|
|
||||||
|
### Nginx (optional)
|
||||||
|
|
||||||
|
*Adjust the listening ports*
|
||||||
|
|
||||||
|
Adjust the listening ports in two places:
|
||||||
|
`/nginx/api.conf`: Change the listening port in one or both servers
|
||||||
|
`/docker-compose.yaml`: Change the 'ports' entries in the nginx service. The first part is the host port to bind, the second part is the container port to bind. For example, `9000:3440` would route inbound requests for `localhost:9000` on to the nginx container (and the nginx service) at port 3440.
|
||||||
|
|
||||||
|
*Adding Nginx without SSL*
|
||||||
|
|
||||||
|
If you want to use Nginx without SSL, comment out the second `server` object in `nginx/api.conf` so you're only handling basic HTTP requests. Run `docker compose up -d --built nginx`.
|
||||||
|
|
||||||
|
*Adding Nginx with SSL*
|
||||||
|
|
||||||
|
You can use the commands in the other file of `/nginx` to generate self-signed certificates for the domain you're hosting this at. Certbot will remotely check routing for that domain to itself and assign a certificate if valid, based on the well known acme challenge already set up in the `/nginx/api.conf` file.
|
||||||
|
|
||||||
|
> IMPORTANT: If you want to do this, you'll need to comment out the second `server` object in `/nginx/api.conf` FIRST so Nginx doesn't crash when it looks for not-yet-existing SSL certs, and can respond on port 9230 (or whatever you've set it to) with the response. Then launch, ensure Nginx is running, then launch a Certbot command to check the domain endpoint.
|
||||||
|
|
||||||
|
## Services in Docker Compose
|
||||||
|
|
||||||
|
### DB
|
||||||
|
|
||||||
|
*MySQL database with relatively default configuration*
|
||||||
|
|
||||||
|
- references .env file for SQL root password and other information
|
||||||
|
- exposes SQL interface on port 12730 of the host - inbound access still subject to firewall restrictions
|
||||||
|
- natively accessible at the unchanged 3306 port for any containers within the same Compose project's default bridge network -- i.e., the api service in this project references the mysql container by hostname 'db', matching the service name
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
*Node.js-served API using express.js and Sequelize. Documentation in [OpenAPI 3.0 spec](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md).*
|
||||||
|
|
||||||
|
- references DB connectivity details & shared secret for authenticating HTTP requests from .env file
|
||||||
|
- contains controllers, models, and routes
|
||||||
|
- controllers
|
||||||
|
- defines actions to be taken in response to incoming web requests
|
||||||
|
- includes data processing, database actions, and response actions
|
||||||
|
- models
|
||||||
|
- defines entity models - entity default fields and constraints
|
||||||
|
- routes
|
||||||
|
- pairs API endpoints with controller definitions
|
||||||
|
- junctionModels
|
||||||
|
- a folder for modeling entities that serve explicitly as junction points -- most notably 'event' formats
|
||||||
|
|
||||||
|
`db/index.js` imports models and defines relations among them. it exports a 'db' object containing the Sequelize instance as well as all imported models. This is referenced in the primary application, as a **database synchronization is performed at runtime**.
|
||||||
|
|
||||||
|
`/index.js` Contains the main application logic and express configuration. It currently uses CORS and a very in-development section, but with basic authentication through standard bearer tokens. a JWT alternative is commented out.
|
||||||
|
|
||||||
|
> This main logic also instructs Sequelize to sync the database on launch. **`force: true`, all data will be wiped** as tables are dropped to account for possible changes in the schema definitions. This is a development tool and should be disabled in production.
|
||||||
|
|
||||||
|
|
||||||
|
### Nginx and Certbot
|
||||||
|
|
||||||
|
Optional nginx instance for serving the API via reverse proxy on an external port of your choosing. This include a certbot container for acquiring a self-signed certificate, and bind mounts can be changed to utilize them to secure the API with SSL.
|
||||||
|
|
||||||
|
|
||||||
|
### 17th-web
|
||||||
|
|
||||||
|
*This is a Vue instance designed to interface with the API and display the information to users, as well as permit administrators to manipulate the database. This has fallen behind active API development and may no longer work. This can be replaced with AJ's demo site if favored.*
|
||||||
|
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Bootstrap](https://getbootstrap.com/)
|
||||||
|
- [Vue 3 CRUD Tutorial](https://www.bezkoder.com/vue-3-crud/)
|
||||||
|
- [Node.js & Sequelize (MySQL) Tutorial](https://www.bezkoder.com/node-js-express-sequelize-mysql/)
|
||||||
|
|||||||
15
api/Dockerfile
Normal file
15
api/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# nodejs container
|
||||||
|
FROM node:latest
|
||||||
|
|
||||||
|
# Create app directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Bundle app source
|
||||||
|
# COPY . /app
|
||||||
|
|
||||||
|
# Install app dependencies
|
||||||
|
# RUN npm install
|
||||||
|
|
||||||
|
EXPOSE $API_PORT
|
||||||
|
CMD [ "npm", "run", "start" ]
|
||||||
|
|
||||||
19
api/db/config.js
Normal file
19
api/db/config.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// dotenv
|
||||||
|
// .config()
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
HOST: process.env.DB_HOST,
|
||||||
|
PORT: process.env.DB_PORT,
|
||||||
|
// USER: process.env.DB_USER,
|
||||||
|
USER: 'root',
|
||||||
|
// PASSWORD: process.env.DB_PASSWORD,
|
||||||
|
PASSWORD: process.env.DB_ROOT_PASSWORD,
|
||||||
|
DATABASE: process.env.DB_DATABASE,
|
||||||
|
dialect: "mysql",
|
||||||
|
pool: {
|
||||||
|
max: 5,
|
||||||
|
min: 0,
|
||||||
|
acquire: 30000,
|
||||||
|
idle: 10000
|
||||||
|
}
|
||||||
|
};
|
||||||
70
api/db/controllers/Award.controller.js
Normal file
70
api/db/controllers/Award.controller.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
const db = require("..");
|
||||||
|
const Award = db.Award;
|
||||||
|
const Op = db.Sequelize.Op;
|
||||||
|
|
||||||
|
// Create and Save a new Award
|
||||||
|
exports.create = (req, res) => {
|
||||||
|
// Validate
|
||||||
|
if (!req.body) {
|
||||||
|
res.status(400).send({
|
||||||
|
message: "Body content can not be empty!"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const awards = []
|
||||||
|
|
||||||
|
if (Array.isArray(req.body)) {
|
||||||
|
awards.push(...req.body)
|
||||||
|
} else {
|
||||||
|
awards.push(req.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
const promises = awards.map(award => {
|
||||||
|
return Award.create(award)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create
|
||||||
|
const successes = []
|
||||||
|
const failures = []
|
||||||
|
Promise.allSettled(promises)
|
||||||
|
.then(data => {
|
||||||
|
if (data.every(result => result.status === 'fulfilled')) {
|
||||||
|
res.status(201).send({
|
||||||
|
message: "All awards were created successfully.",
|
||||||
|
successes: data.map(result => result.value),
|
||||||
|
failures: [],
|
||||||
|
})
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.forEach(result => {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
successes.push(result.value)
|
||||||
|
} else {
|
||||||
|
failures.push(result.reason.errors)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (successes.length === 0) {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
"Failed to create any Awards.",
|
||||||
|
failures: failures,
|
||||||
|
successes: successes,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(207).send({
|
||||||
|
message: "Some Awards were created successfully.",
|
||||||
|
successes: successes,
|
||||||
|
failures: failures,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while creating the Award.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
176
api/db/controllers/Course.controller.js
Normal file
176
api/db/controllers/Course.controller.js
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
const db = require("../");
|
||||||
|
const Course = db.Course;
|
||||||
|
const Op = db.Sequelize.Op;
|
||||||
|
|
||||||
|
// Create and Save a new Course
|
||||||
|
exports.create = (req, res) => {
|
||||||
|
// Validate
|
||||||
|
if (!req.body) {
|
||||||
|
res.status(400).send({
|
||||||
|
message: "Body content can not be empty!"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create
|
||||||
|
const courses = []
|
||||||
|
|
||||||
|
if (Array.isArray(req.body)) {
|
||||||
|
courses.push(...req.body)
|
||||||
|
} else {
|
||||||
|
courses.push(req.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
const promises = []
|
||||||
|
courses.forEach(course => {
|
||||||
|
promises.push(Course.create(course))
|
||||||
|
});
|
||||||
|
|
||||||
|
const successes = []
|
||||||
|
const failures = []
|
||||||
|
Promise.allSettled(promises)
|
||||||
|
.then(data => {
|
||||||
|
if (data.every(result => result.status === 'fulfilled')) {
|
||||||
|
res.status(201).send({
|
||||||
|
message: "All courses were created successfully.",
|
||||||
|
successes: data.map(result => result.value),
|
||||||
|
failures: [],
|
||||||
|
})
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.forEach(result => {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
successes.push(result.value)
|
||||||
|
} else {
|
||||||
|
failures.push(result.reason)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
res.status(207).send({
|
||||||
|
message: "Some courses were created successfully.",
|
||||||
|
successes: successes,
|
||||||
|
failures: failures,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while creating the Course.",
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Retrieve all Courses from the database.
|
||||||
|
exports.findAll = (req, res) => {
|
||||||
|
const name = req.query.name;
|
||||||
|
var condition = name ? { name: { [Op.like]: `%${name}%` } } : null;
|
||||||
|
|
||||||
|
Course.findAll({ where: condition, include: ['trainingsHeld'] })
|
||||||
|
.then(data => {
|
||||||
|
res.send(data);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving courses."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find a single Course with an id
|
||||||
|
exports.findOne = (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
|
||||||
|
Course.findByPk(id)
|
||||||
|
.then(data => {
|
||||||
|
res.send(data);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message: "Error retrieving Course with id=" + id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update a Course by the id in the request
|
||||||
|
exports.update = (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
|
||||||
|
Course.update(req.body, {
|
||||||
|
where: { id: id }
|
||||||
|
})
|
||||||
|
.then(num => {
|
||||||
|
if (num == 1) {
|
||||||
|
res.send({
|
||||||
|
message: "Course was updated successfully."
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.send({
|
||||||
|
message: `Cannot update Course with id=${id}. Maybe Course was not found or req.body is empty!`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message: "Error updating Course with id=" + id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete a Course with the specified id in the request
|
||||||
|
exports.delete = (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
|
||||||
|
Course.destroy({
|
||||||
|
where: { id: id }
|
||||||
|
})
|
||||||
|
.then(num => {
|
||||||
|
if (num == 1) {
|
||||||
|
res.send({
|
||||||
|
message: "Course was deleted successfully!"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.send({
|
||||||
|
message: `Cannot delete Course with id=${id}. Maybe Course was not found!`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message: "Could not delete Course with id=" + id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete all Courses from the database.
|
||||||
|
exports.deleteAll = (req, res) => {
|
||||||
|
Course.destroy({
|
||||||
|
where: {},
|
||||||
|
truncate: false
|
||||||
|
})
|
||||||
|
.then(nums => {
|
||||||
|
res.send({ message: `${nums} Courses were deleted successfully!` });
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while removing all courses."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find all published Courses
|
||||||
|
exports.findAllPublished = (req, res) => {
|
||||||
|
Course.findAll({ where: { published: true } })
|
||||||
|
.then(data => {
|
||||||
|
res.send(data);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving courses."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
100
api/db/controllers/Member.controller.js
Normal file
100
api/db/controllers/Member.controller.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
const db = require("..");
|
||||||
|
const Member = db.Member;
|
||||||
|
const Op = db.Sequelize.Op;
|
||||||
|
|
||||||
|
// Create and Save a new Member
|
||||||
|
exports.create = (req, res) => {
|
||||||
|
// Validate
|
||||||
|
if (!req.body) {
|
||||||
|
res.status(400).send({
|
||||||
|
message: "Body content can not be empty!"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create
|
||||||
|
const members = []
|
||||||
|
|
||||||
|
if (Array.isArray(req.body)) {
|
||||||
|
members.push(...req.body)
|
||||||
|
} else {
|
||||||
|
members.push(req.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
const promises = members.map(member => {
|
||||||
|
return Member.create(member)
|
||||||
|
.then(memberObj => {
|
||||||
|
if (req.body.rankId) {
|
||||||
|
db.Rank.findByPk(req.body.rankId)
|
||||||
|
.then(rank => {
|
||||||
|
memberObj.setRank(rank)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (req.body.statusId) {
|
||||||
|
db.MemberStatus.findByPk(req.body.statusId)
|
||||||
|
.then(status => {
|
||||||
|
memberObj.setStatus(status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return memberObj
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create
|
||||||
|
const successes = []
|
||||||
|
const failures = []
|
||||||
|
Promise.allSettled(promises)
|
||||||
|
.then(data => {
|
||||||
|
if (data.every(result => result.status === 'fulfilled')) {
|
||||||
|
res.status(201).send({
|
||||||
|
message: "All members were created successfully.",
|
||||||
|
successes: data.map(result => result.value),
|
||||||
|
failures: [],
|
||||||
|
})
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.forEach(result => {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
successes.push(result.value)
|
||||||
|
} else {
|
||||||
|
failures.push(result.reason.errors)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (successes.length === 0) {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
"Failed to create any Members.",
|
||||||
|
failures: failures,
|
||||||
|
successes: successes,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(207).send({
|
||||||
|
message: "Some Members were created successfully.",
|
||||||
|
successes: successes,
|
||||||
|
failures: failures,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while creating the Member.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Retrieve all Members from the database.
|
||||||
|
exports.findAll = (req, res) => {
|
||||||
|
Member.findAll()
|
||||||
|
.then(data => {
|
||||||
|
res.send(data);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving members."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
70
api/db/controllers/Rank.controller.js
Normal file
70
api/db/controllers/Rank.controller.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
const db = require("..");
|
||||||
|
const Rank = db.Rank;
|
||||||
|
const Op = db.Sequelize.Op;
|
||||||
|
|
||||||
|
// Create and Save a new Rank
|
||||||
|
exports.create = (req, res) => {
|
||||||
|
// Validate
|
||||||
|
if (!req.body) {
|
||||||
|
res.status(400).send({
|
||||||
|
message: "Body content can not be empty!"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ranks = []
|
||||||
|
|
||||||
|
if (Array.isArray(req.body)) {
|
||||||
|
ranks.push(...req.body)
|
||||||
|
} else {
|
||||||
|
ranks.push(req.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
const promises = ranks.map(rank => {
|
||||||
|
return Rank.create(rank)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create
|
||||||
|
const successes = []
|
||||||
|
const failures = []
|
||||||
|
Promise.allSettled(promises)
|
||||||
|
.then(data => {
|
||||||
|
if (data.every(result => result.status === 'fulfilled')) {
|
||||||
|
res.status(201).send({
|
||||||
|
message: "All ranks were created successfully.",
|
||||||
|
successes: data.map(result => result.value),
|
||||||
|
failures: [],
|
||||||
|
})
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.forEach(result => {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
successes.push(result.value)
|
||||||
|
} else {
|
||||||
|
failures.push(result.reason.errors)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (successes.length === 0) {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
"Failed to create any Ranks.",
|
||||||
|
failures: failures,
|
||||||
|
successes: successes,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(207).send({
|
||||||
|
message: "Some Ranks were created successfully.",
|
||||||
|
successes: successes,
|
||||||
|
failures: failures,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while creating the Rank.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
260
api/db/index.js
Normal file
260
api/db/index.js
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
// require dbconfig
|
||||||
|
const dbConfig = require("./config.js");
|
||||||
|
|
||||||
|
|
||||||
|
// create connection to database
|
||||||
|
const Sequelize = require("sequelize");
|
||||||
|
const sequelize = new Sequelize(
|
||||||
|
dbConfig.DATABASE,
|
||||||
|
dbConfig.USER,
|
||||||
|
dbConfig.PASSWORD,
|
||||||
|
{
|
||||||
|
host: dbConfig.HOST,
|
||||||
|
port: dbConfig.PORT,
|
||||||
|
dialect: dbConfig.dialect,
|
||||||
|
|
||||||
|
pool: {
|
||||||
|
max: dbConfig.pool.max,
|
||||||
|
min: dbConfig.pool.min,
|
||||||
|
acquire: dbConfig.pool.acquire,
|
||||||
|
idle: dbConfig.pool.idle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
sequelize.authenticate();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Unable to connect to the database:', error);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const db = {};
|
||||||
|
|
||||||
|
db.Sequelize = Sequelize;
|
||||||
|
db.instance = sequelize;
|
||||||
|
|
||||||
|
db.Member = db.instance.define("Member", require("./models/Member.model.js"), { paranoid: true })
|
||||||
|
db.Award = db.instance.define("Award", require("./models/Award.model.js"), { paranoid: true })
|
||||||
|
db.Course = db.instance.define("Course", require("./models/Course.model.js"), { paranoid: true })
|
||||||
|
db.Rank = db.instance.define("Rank", require("./models/Rank.model.js"), { paranoid: true })
|
||||||
|
|
||||||
|
db.AwardAction = db.instance.define("AwardAction", require("./junctionModels/AwardAction.js"), { paranoid: true })
|
||||||
|
db.CourseEventTrainingReport = db.instance.define("CourseEventTrainingReport", require("./junctionModels/CourseEventTrainingReport.model.js"), { paranoid: true })
|
||||||
|
db.CourseEvent = db.instance.define("CourseEvent", require("./models/CourseEvent.js"), { paranoid: true })
|
||||||
|
|
||||||
|
// Members have ranks
|
||||||
|
db.Rank.hasMany(db.Member, {
|
||||||
|
as: "members",
|
||||||
|
foreignKey: "rankId"
|
||||||
|
})
|
||||||
|
db.Member.belongsTo(db.Rank, {
|
||||||
|
as: "rank",
|
||||||
|
foreignKey: "rankId"
|
||||||
|
})
|
||||||
|
|
||||||
|
// Members have statuses
|
||||||
|
db.MemberStatus = db.instance.define("MemberStatus", require("./models/MemberStatus.model.js"), { paranoid: true })
|
||||||
|
db.MemberStatus.hasMany(db.Member, {
|
||||||
|
as: "members",
|
||||||
|
foreignKey: "statusId"
|
||||||
|
})
|
||||||
|
db.Member.belongsTo(db.MemberStatus, {
|
||||||
|
as: "status",
|
||||||
|
foreignKey: "statusId"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// * AWARDS
|
||||||
|
// Awards have a creator
|
||||||
|
db.Award.belongsTo(db.Member, {
|
||||||
|
as: "createdBy",
|
||||||
|
foreignKey: "createdById"
|
||||||
|
})
|
||||||
|
db.Member.hasMany(db.Award, {
|
||||||
|
as: "awardsCreated",
|
||||||
|
foreignKey: "createdById"
|
||||||
|
})
|
||||||
|
// Awards have a last modified by
|
||||||
|
db.Award.belongsTo(db.Member, {
|
||||||
|
as: "lastModifiedBy",
|
||||||
|
foreignKey: "lastModifiedById"
|
||||||
|
})
|
||||||
|
db.Member.hasMany(db.Award, {
|
||||||
|
as: "awardsLastModified",
|
||||||
|
foreignKey: "lastModifiedById"
|
||||||
|
})
|
||||||
|
// Members are granted awards
|
||||||
|
db.Award.belongsToMany(db.Member, {
|
||||||
|
through: db.AwardAction,
|
||||||
|
as: "awardHolders",
|
||||||
|
foreignKey: "recipientId",
|
||||||
|
});
|
||||||
|
db.Member.belongsToMany(db.Award, {
|
||||||
|
through: db.AwardAction,
|
||||||
|
as: "awardsEarned",
|
||||||
|
foreignKey: "awardId"
|
||||||
|
});
|
||||||
|
// * AWARDS GRANTED/REVOKED
|
||||||
|
// Instances of award grants have a creator
|
||||||
|
db.AwardAction.belongsTo(db.Member, {
|
||||||
|
as: "createdBy",
|
||||||
|
foreignKey: "createdById"
|
||||||
|
})
|
||||||
|
db.Member.hasMany(db.AwardAction, {
|
||||||
|
as: "awardGrantsCreated",
|
||||||
|
foreignKey: "createdById"
|
||||||
|
})
|
||||||
|
// Instances of award grants have a last modified by
|
||||||
|
db.AwardAction.belongsTo(db.Member, {
|
||||||
|
as: "lastModifiedBy",
|
||||||
|
foreignKey: "lastModifiedById"
|
||||||
|
})
|
||||||
|
db.Member.hasMany(db.AwardAction, {
|
||||||
|
as: "awardGrantsLastModified",
|
||||||
|
foreignKey: "lastModifiedById"
|
||||||
|
})
|
||||||
|
// Instances of award grants have the acting member
|
||||||
|
db.AwardAction.belongsTo(db.Member, {
|
||||||
|
as: "actor",
|
||||||
|
foreignKey: "actorId"
|
||||||
|
})
|
||||||
|
db.Member.hasMany(db.AwardAction, {
|
||||||
|
as: "awardGrantsGiven",
|
||||||
|
foreignKey: "actorId"
|
||||||
|
})
|
||||||
|
// Instances of award grants have the recipient
|
||||||
|
db.AwardAction.belongsTo(db.Member, {
|
||||||
|
as: "recipient",
|
||||||
|
foreignKey: "recipientId"
|
||||||
|
})
|
||||||
|
db.Member.hasMany(db.AwardAction, {
|
||||||
|
as: "awardGrantsReceived",
|
||||||
|
foreignKey: "recipientId"
|
||||||
|
})
|
||||||
|
// Instances of award grants have the award
|
||||||
|
db.AwardAction.belongsTo(db.Award, {
|
||||||
|
as: "award",
|
||||||
|
foreignKey: "awardId"
|
||||||
|
})
|
||||||
|
db.Award.hasMany(db.AwardAction, {
|
||||||
|
as: "awardGrants",
|
||||||
|
foreignKey: "awardId"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// * COURSE EVENTS
|
||||||
|
// Events have a creator
|
||||||
|
db.CourseEvent.belongsTo(db.Member, {
|
||||||
|
as: "createdBy",
|
||||||
|
foreignKey: "createdById"
|
||||||
|
})
|
||||||
|
db.Member.hasMany(db.CourseEvent, {
|
||||||
|
as: "courseEventsCreated",
|
||||||
|
foreignKey: "createdById"
|
||||||
|
})
|
||||||
|
// Events have a last modified by
|
||||||
|
db.CourseEvent.belongsTo(db.Member, {
|
||||||
|
as: "lastModifiedBy",
|
||||||
|
foreignKey: "lastModifiedById"
|
||||||
|
})
|
||||||
|
db.Member.hasMany(db.CourseEvent, {
|
||||||
|
as: "courseEventsLastModified",
|
||||||
|
foreignKey: "lastModifiedById"
|
||||||
|
})
|
||||||
|
// Events have a course that's taught
|
||||||
|
db.CourseEvent.belongsTo(db.Course, {
|
||||||
|
as: "courseTaught",
|
||||||
|
foreignKey: "courseTaughtId"
|
||||||
|
})
|
||||||
|
db.Course.hasMany(db.CourseEvent, {
|
||||||
|
as: "trainingsHeld",
|
||||||
|
foreignKey: "courseTaughtId"
|
||||||
|
})
|
||||||
|
// Events have one or more trainer
|
||||||
|
db.CourseEvent.belongsToMany(db.Member, {
|
||||||
|
through: "CourseEventsTrainers",
|
||||||
|
as: "trainers",
|
||||||
|
foreignKey: "trainerId"
|
||||||
|
})
|
||||||
|
db.Member.belongsToMany(db.CourseEvent, {
|
||||||
|
through: "CourseEventsTrainers",
|
||||||
|
as: "courseEventsTaught",
|
||||||
|
foreignKey: "courseEventId"
|
||||||
|
})
|
||||||
|
// Events have one or more observer
|
||||||
|
db.CourseEvent.belongsToMany(db.Member, {
|
||||||
|
through: "CourseEventsObservers",
|
||||||
|
as: "observers",
|
||||||
|
foreignKey: "courseEventId"
|
||||||
|
})
|
||||||
|
db.Member.belongsToMany(db.CourseEvent, {
|
||||||
|
through: "CourseEventsObservers",
|
||||||
|
as: "courseEventsObserved",
|
||||||
|
foreignKey: "observerId"
|
||||||
|
})
|
||||||
|
// Events have one or more attendees, each of which passed or did not
|
||||||
|
db.CourseEvent.belongsToMany(db.Member, {
|
||||||
|
through: db.CourseEventTrainingReport,
|
||||||
|
as: "attendees",
|
||||||
|
foreignKey: "attendeeId"
|
||||||
|
});
|
||||||
|
db.Member.belongsToMany(db.CourseEvent, {
|
||||||
|
through: db.CourseEventTrainingReport,
|
||||||
|
as: "courseEventsAttended",
|
||||||
|
foreignKey: "courseEventId"
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// * COURSES
|
||||||
|
// Courses have a creator
|
||||||
|
db.Course.belongsTo(db.Member, {
|
||||||
|
as: "createdBy",
|
||||||
|
foreignKey: "createdById"
|
||||||
|
})
|
||||||
|
db.Member.hasMany(db.Course, {
|
||||||
|
as: "coursesCreated",
|
||||||
|
foreignKey: "createdById"
|
||||||
|
})
|
||||||
|
// Courses have a last modified by
|
||||||
|
db.Course.belongsTo(db.Member, {
|
||||||
|
as: "lastModifiedBy",
|
||||||
|
foreignKey: "lastModifiedById"
|
||||||
|
})
|
||||||
|
db.Member.hasMany(db.Course, {
|
||||||
|
as: "coursesLastModified",
|
||||||
|
foreignKey: "lastModifiedById"
|
||||||
|
})
|
||||||
|
// Courses have SMEs
|
||||||
|
db.Course.belongsToMany(db.Member, {
|
||||||
|
through: "CoursesSME",
|
||||||
|
as: "sme",
|
||||||
|
foreignKey: "smeId"
|
||||||
|
})
|
||||||
|
db.Member.belongsToMany(db.Course, {
|
||||||
|
through: "CoursesSME",
|
||||||
|
as: "coursesSMEFor",
|
||||||
|
foreignKey: "courseId"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Courses belong to award paths
|
||||||
|
db.Award.belongsToMany(db.Course, {
|
||||||
|
through: "CoursesAwards",
|
||||||
|
as: "coursesRequired",
|
||||||
|
foreignKey: "courseId"
|
||||||
|
});
|
||||||
|
// Awards have pre-requisite courses
|
||||||
|
db.Course.belongsToMany(db.Award, {
|
||||||
|
through: "CoursesAwards",
|
||||||
|
as: "possibleAwards",
|
||||||
|
foreignKey: "awardId"
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = db;
|
||||||
34
api/db/junctionModels/AwardAction.js
Normal file
34
api/db/junctionModels/AwardAction.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const Sequelize = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
actionDate: {
|
||||||
|
type: Sequelize.DataTypes.DATEONLY,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: Sequelize.DataTypes.NOW
|
||||||
|
},
|
||||||
|
isGrantEvent: {
|
||||||
|
type: Sequelize.DataTypes.BOOLEAN,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
actorId: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
recipientId: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
awardId: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
createdById: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
lastModifiedById: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
21
api/db/junctionModels/courseEventTrainingReport.model.js
Normal file
21
api/db/junctionModels/courseEventTrainingReport.model.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const Sequelize = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
attendeeId: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
courseEventId: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
passed: {
|
||||||
|
type: Sequelize.DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
type: Sequelize.DataTypes.STRING(500),
|
||||||
|
allowNull: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
37
api/db/models/Award.model.js
Normal file
37
api/db/models/Award.model.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const Sequelize = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: {
|
||||||
|
type: Sequelize.DataTypes.STRING(100),
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
shortName: {
|
||||||
|
type: Sequelize.DataTypes.STRING(70),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: Sequelize.DataTypes.STRING(1000),
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: Sequelize.DataTypes.STRING(100),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
imageUrl: {
|
||||||
|
type: Sequelize.DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
isUrl: true
|
||||||
|
},
|
||||||
|
footprint: {
|
||||||
|
type: Sequelize.DataTypes.STRING(45),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
createdById: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
lastModifiedById: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
32
api/db/models/Course.model.js
Normal file
32
api/db/models/Course.model.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const Sequelize = require("sequelize");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: {
|
||||||
|
type: Sequelize.DataTypes.STRING(100),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
shortName: {
|
||||||
|
type: Sequelize.DataTypes.STRING(70),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: Sequelize.DataTypes.STRING(100),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: Sequelize.DataTypes.STRING(1000),
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
imageUrl: {
|
||||||
|
type: Sequelize.DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
createdById: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
lastModifiedById: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
25
api/db/models/CourseEvent.js
Normal file
25
api/db/models/CourseEvent.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
const Sequelize = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
notes: {
|
||||||
|
type: Sequelize.DataTypes.STRING(1000),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
runDate: {
|
||||||
|
type: Sequelize.DataTypes.DATEONLY,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: Sequelize.DataTypes.NOW
|
||||||
|
},
|
||||||
|
courseTaughtId: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
createdById: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
lastModifiedById: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
53
api/db/models/Member.model.js
Normal file
53
api/db/models/Member.model.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
const Sequelize = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: {
|
||||||
|
type: Sequelize.DataTypes.STRING(100),
|
||||||
|
allowNull: false,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: Sequelize.DataTypes.STRING(100),
|
||||||
|
allowNull: true,
|
||||||
|
// validate: {
|
||||||
|
// isEmail: true
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: Sequelize.DataTypes.STRING(100),
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
website: {
|
||||||
|
type: Sequelize.DataTypes.STRING(240),
|
||||||
|
allowNull: true,
|
||||||
|
// validate: {
|
||||||
|
// isUrl: true
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
steamId64: {
|
||||||
|
type: Sequelize.DataTypes.STRING(17),
|
||||||
|
allowNull: true,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
steamProfileName: {
|
||||||
|
type: Sequelize.DataTypes.STRING(32),
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
discordId: {
|
||||||
|
type: Sequelize.DataTypes.STRING(18),
|
||||||
|
unique: true,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
discordUsername: {
|
||||||
|
type: Sequelize.DataTypes.STRING(32),
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
createdById: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
lastModifiedById: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
16
api/db/models/MemberStatus.model.js
Normal file
16
api/db/models/MemberStatus.model.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const Sequelize = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: {
|
||||||
|
type: Sequelize.DataTypes.STRING(100),
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
createdById: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
lastModifiedById: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
0
api/db/models/Mission.model.js
Normal file
0
api/db/models/Mission.model.js
Normal file
35
api/db/models/Rank.model.js
Normal file
35
api/db/models/Rank.model.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
const Sequelize = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: {
|
||||||
|
type: Sequelize.DataTypes.STRING(100),
|
||||||
|
allowNull: false,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
shortName: {
|
||||||
|
type: Sequelize.DataTypes.STRING(70),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: Sequelize.DataTypes.STRING(100),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
sortId: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
imageUrl: {
|
||||||
|
type: Sequelize.DataTypes.STRING(240),
|
||||||
|
allowNull: true,
|
||||||
|
isUrl: true
|
||||||
|
},
|
||||||
|
createdById: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
lastModifiedById: {
|
||||||
|
type: Sequelize.DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
98
api/db/routes/Award.route.js
Normal file
98
api/db/routes/Award.route.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
|
||||||
|
const award = require("../controllers/Award.controller.js");
|
||||||
|
|
||||||
|
const db = require("..");
|
||||||
|
|
||||||
|
var router = require("express").Router();
|
||||||
|
|
||||||
|
// Create a new Award
|
||||||
|
router.post("/", award.create);
|
||||||
|
|
||||||
|
// GET AWARD
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
const id = req.query.id;
|
||||||
|
if (!id) {
|
||||||
|
return db.Award.findAll()
|
||||||
|
.then(results => res.send(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Award.findByPk(id)
|
||||||
|
.then(async (award) => {
|
||||||
|
if (award === null) {
|
||||||
|
res.status(404).send({
|
||||||
|
message: `Award with id=${id} was not found!`
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.send(award)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving awards."
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET AWARD DETAILS
|
||||||
|
router.get("/details", async (req, res) => {
|
||||||
|
const id = req.query.id;
|
||||||
|
if (!id) {
|
||||||
|
res.status(400).send({
|
||||||
|
message: "Award id cannot be empty!"
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return db.Award.findByPk(id, {
|
||||||
|
include: [
|
||||||
|
'awardHolders',
|
||||||
|
'coursesRequired'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.then(async (award) => {
|
||||||
|
if (award === null) {
|
||||||
|
res.status(404).send({
|
||||||
|
message: `Award with id=${id} was not found!`
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.send(award)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving awards."
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// GET CATEGORIES
|
||||||
|
router.get("/categories", async (req, res) => {
|
||||||
|
return db.Award.findAll({
|
||||||
|
attributes: ['category'],
|
||||||
|
group: ['category']
|
||||||
|
})
|
||||||
|
.then(async (awardCategories) => {
|
||||||
|
if (awardCategories === null) {
|
||||||
|
res.status(404).send({
|
||||||
|
message: `Award categories were not found!`
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.send(awardCategories.map(awardCategory => awardCategory.category))
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving award categories."
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
apiPath: "/api/awards",
|
||||||
|
apiRouter: router
|
||||||
|
};
|
||||||
96
api/db/routes/Course.route.js
Normal file
96
api/db/routes/Course.route.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
const courses = require("../controllers/Course.controller.js");
|
||||||
|
|
||||||
|
const db = require("..");
|
||||||
|
|
||||||
|
var router = require("express").Router();
|
||||||
|
|
||||||
|
// Create a new Course
|
||||||
|
router.post("/", courses.create);
|
||||||
|
|
||||||
|
// GET AWARD
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
const id = req.query.id;
|
||||||
|
if (!id) {
|
||||||
|
return db.Course.findAll()
|
||||||
|
.then(results => res.send(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Course.findByPk(id)
|
||||||
|
.then(async (course) => {
|
||||||
|
if (course === null) {
|
||||||
|
res.status(404).send({
|
||||||
|
message: `Course with id=${id} was not found!`
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.send(course)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving courses."
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET AWARD DETAILS
|
||||||
|
router.get("/details", async (req, res) => {
|
||||||
|
const id = req.query.id;
|
||||||
|
if (!id) {
|
||||||
|
res.status(400).send({
|
||||||
|
message: "Course id cannot be empty!"
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return db.Course.findByPk(id, {
|
||||||
|
include: [
|
||||||
|
'trainingsHeld',
|
||||||
|
'possibleAwards',
|
||||||
|
'sme'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.then(async (course) => {
|
||||||
|
if (course === null) {
|
||||||
|
res.status(404).send({
|
||||||
|
message: `Course with id=${id} was not found!`
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.send(course)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving courses."
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// GET CATEGORIES
|
||||||
|
router.get("/categories", async (req, res) => {
|
||||||
|
return db.Course.findAll({
|
||||||
|
attributes: ['category'],
|
||||||
|
group: ['category']
|
||||||
|
})
|
||||||
|
.then(async (courseCategories) => {
|
||||||
|
if (courseCategories === null) {
|
||||||
|
res.status(404).send({
|
||||||
|
message: `Course categories were not found!`
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.send(courseCategories.map(courseCategory => courseCategory.category))
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving course categories."
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
apiPath: "/api/courses",
|
||||||
|
apiRouter: router
|
||||||
|
};
|
||||||
231
api/db/routes/Member.route.js
Normal file
231
api/db/routes/Member.route.js
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
|
||||||
|
const member = require("../controllers/Member.controller.js");
|
||||||
|
|
||||||
|
const db = require("..");
|
||||||
|
|
||||||
|
var router = require("express").Router();
|
||||||
|
|
||||||
|
// Create a new Member
|
||||||
|
router.post("/", member.create);
|
||||||
|
// Retrieve all Members
|
||||||
|
router.get("/", member.findAll);
|
||||||
|
|
||||||
|
// GET MEMBER
|
||||||
|
router.get("/:id", async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
if (!id) {
|
||||||
|
res.status(400).send({
|
||||||
|
message: "Member id cannot be empty!"
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return db.Member.findByPk(id)
|
||||||
|
.then(async (member) => {
|
||||||
|
if (member === null) {
|
||||||
|
res.status(404).send({
|
||||||
|
message: `Member with id=${id} was not found!`
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.send(member)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving members."
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET MEMBER DETAILS
|
||||||
|
router.get("/:id/details", async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
if (!id) {
|
||||||
|
res.status(400).send({
|
||||||
|
message: "Member id cannot be empty!"
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return db.Member.findByPk(id, {
|
||||||
|
include: [
|
||||||
|
'rank',
|
||||||
|
'status',
|
||||||
|
'awards',
|
||||||
|
'coursesSMEFor',
|
||||||
|
'coursesTaught',
|
||||||
|
'coursesAttended'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.then(async (member) => {
|
||||||
|
if (member === null) {
|
||||||
|
res.status(404).send({
|
||||||
|
message: `Member with id=${id} was not found!`
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.send(member)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving members."
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// COURSES TAUGHT
|
||||||
|
router.get("/:id/courses/taught", async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
if (!id) {
|
||||||
|
res.status(400).send({
|
||||||
|
message: "Member id cannot be empty!"
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return db.Member.findByPk(id)
|
||||||
|
.then(async (member) => {
|
||||||
|
if (member === null) {
|
||||||
|
res.status(404).send({
|
||||||
|
message: `Member with id=${id} was not found!`
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const courses = await member.getCoursesSMEFor()
|
||||||
|
res.send(courses)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving members."
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// COURSES ATTENDED
|
||||||
|
router.get("/:id/courses/attended", async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
if (!id) {
|
||||||
|
res.status(400).send({
|
||||||
|
message: "Member id cannot be empty!"
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return db.Member.findByPk(id)
|
||||||
|
.then(async (member) => {
|
||||||
|
if (member === null) {
|
||||||
|
res.status(404).send({
|
||||||
|
message: `Member with id=${id} was not found!`
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const courses = await member.getCoursesAttended()
|
||||||
|
res.send(courses)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving members."
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// COURSES SME FOR
|
||||||
|
router.get("/:id/courses/sme", async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
if (!id) {
|
||||||
|
res.status(400).send({
|
||||||
|
message: "Member id cannot be empty!"
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return db.Member.findByPk(id)
|
||||||
|
.then(async (member) => {
|
||||||
|
if (member === null) {
|
||||||
|
res.status(404).send({
|
||||||
|
message: `Member with id=${id} was not found!`
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const courses = await member.getCoursesSMEFor()
|
||||||
|
res.send(courses)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving members."
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// UPDATE MEMBER
|
||||||
|
router.put("/:id", async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
if (!id) {
|
||||||
|
res.status(400).send({
|
||||||
|
message: "Member id cannot be empty!"
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return db.Member.findByPk(id)
|
||||||
|
.then(async (member) => {
|
||||||
|
if (member === null) {
|
||||||
|
res.status(404).send({
|
||||||
|
message: `Member with id=${id} was not found!`
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
member.set(req.body)
|
||||||
|
await member.save()
|
||||||
|
res.send(member)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving members.",
|
||||||
|
error: err
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Delete a Member with id
|
||||||
|
router.delete("/:id", async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
if (!id) {
|
||||||
|
res.status(400).send({
|
||||||
|
message: "Member id cannot be empty!"
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return db.Member.findByPk(id)
|
||||||
|
.then(async (member) => {
|
||||||
|
if (member === null) {
|
||||||
|
res.status(404).send({
|
||||||
|
message: `Member with id=${id} was not found!`
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await member.destroy()
|
||||||
|
res.send({
|
||||||
|
deleted: member,
|
||||||
|
message: `Member with id=${id} was deleted!`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving members.",
|
||||||
|
error: err
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete all Members
|
||||||
|
// router.delete("/", member.deleteAll);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
apiPath: "/api/members",
|
||||||
|
apiRouter: router
|
||||||
|
};
|
||||||
95
api/db/routes/Rank.route.js
Normal file
95
api/db/routes/Rank.route.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
const rank = require("../controllers/Rank.controller.js");
|
||||||
|
|
||||||
|
const db = require("..");
|
||||||
|
|
||||||
|
var router = require("express").Router();
|
||||||
|
|
||||||
|
// Create a new Rank
|
||||||
|
router.post("/", rank.create);
|
||||||
|
|
||||||
|
// GET RANK
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
const id = req.query.id;
|
||||||
|
if (!id) {
|
||||||
|
return db.Rank.findAll()
|
||||||
|
.then(results => res.send(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Rank.findByPk(id)
|
||||||
|
.then(async (rank) => {
|
||||||
|
if (rank === null) {
|
||||||
|
res.status(404).send({
|
||||||
|
message: `Rank with id=${id} was not found!`
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.send(rank)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving ranks."
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET RANK DETAILS
|
||||||
|
router.get("/details", async (req, res) => {
|
||||||
|
const id = req.query.id;
|
||||||
|
if (!id) {
|
||||||
|
res.status(400).send({
|
||||||
|
message: "Rank id cannot be empty!"
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return db.Rank.findByPk(id, {
|
||||||
|
include: [
|
||||||
|
'members',
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.then(async (rank) => {
|
||||||
|
if (rank === null) {
|
||||||
|
res.status(404).send({
|
||||||
|
message: `Rank with id=${id} was not found!`
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.send(rank)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving ranks."
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// GET CATEGORIES
|
||||||
|
router.get("/categories", async (req, res) => {
|
||||||
|
return db.Rank.findAll({
|
||||||
|
attributes: ['category'],
|
||||||
|
group: ['category']
|
||||||
|
})
|
||||||
|
.then(async (rankCategories) => {
|
||||||
|
if (rankCategories === null) {
|
||||||
|
res.status(404).send({
|
||||||
|
message: `Rank categories were not found!`
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.send(rankCategories.map(rankCategory => rankCategory.category))
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
res.status(500).send({
|
||||||
|
message:
|
||||||
|
err.message || "Some error occurred while retrieving rank categories."
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
apiPath: "/api/ranks",
|
||||||
|
apiRouter: router
|
||||||
|
};
|
||||||
2529
api/db/sequelize-docgen.js
Normal file
2529
api/db/sequelize-docgen.js
Normal file
File diff suppressed because it is too large
Load Diff
103
api/index.js
Normal file
103
api/index.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
// set up a basic API for MySQL
|
||||||
|
// this is a simple API that will allow us to do basic CRUD operations on our MySQL database
|
||||||
|
|
||||||
|
// import the express module
|
||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors')
|
||||||
|
|
||||||
|
// create an express app
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// enable cors
|
||||||
|
var corsOptions = {
|
||||||
|
// origin: "http://localhost:5173"
|
||||||
|
};
|
||||||
|
// app.use(cors(corsOptions));
|
||||||
|
app.use(cors());
|
||||||
|
|
||||||
|
// parse requests of content-type - application/json
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// parse requests of content-type - application/x-www-form-urlencoded
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
// include all routes
|
||||||
|
const os = require('os');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const routesPath = path.join(__dirname, 'db/routes');
|
||||||
|
const routeFiles = fs.readdirSync(routesPath);
|
||||||
|
|
||||||
|
// auth providers
|
||||||
|
const bearerToken = require('express-bearer-token');
|
||||||
|
// const { expressjwt: jwt } = require("express-jwt");
|
||||||
|
|
||||||
|
for (const file of routeFiles) {
|
||||||
|
const { apiPath, apiRouter } = require(path.join(routesPath, file));
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
apiPath,
|
||||||
|
// JWT Bearer token
|
||||||
|
// jwt({
|
||||||
|
// secret: process.env.JWT_SECRET,
|
||||||
|
// algorithms: ["HS256"],
|
||||||
|
// }),
|
||||||
|
// function (err, req, res, next) {
|
||||||
|
// if (err.name === "UnauthorizedError") {
|
||||||
|
// res.status(401).send("invalid token!");
|
||||||
|
// } else {
|
||||||
|
// next(err);
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// simple plaintext Bearer token
|
||||||
|
bearerToken(),
|
||||||
|
function (req, res, next) {
|
||||||
|
if (req.token === process.env.BEARER_TOKEN) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
res.status(401).send("invalid token!");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
apiRouter
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync/init database
|
||||||
|
const db = require('./db');
|
||||||
|
const dbConfig = require("./db/config.js");
|
||||||
|
// if database doesn't exist, create it
|
||||||
|
db.instance.query('CREATE DATABASE IF NOT EXISTS ' + dbConfig.DATABASE)
|
||||||
|
.then(() => {
|
||||||
|
console.log('Database created')
|
||||||
|
db.instance.sync({
|
||||||
|
force: true
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
|
||||||
|
// configure api docs
|
||||||
|
const swaggerUi = require('swagger-ui-express');
|
||||||
|
const YAML = require('yamljs');
|
||||||
|
const swaggerDocument = YAML.load('openapi.yaml');
|
||||||
|
// local YAML
|
||||||
|
// app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument, {
|
||||||
|
// explorer: true
|
||||||
|
// }));
|
||||||
|
|
||||||
|
// converted from Postman
|
||||||
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument, {
|
||||||
|
explorer: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
Promise.resolve()
|
||||||
|
}).then(() => {
|
||||||
|
|
||||||
|
// set port, listen for requests
|
||||||
|
app.listen(3000, function () {
|
||||||
|
console.log('App running on port ' + 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
409
api/openapi.json
Normal file
409
api/openapi.json
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"info": {
|
||||||
|
"title": "17th Rangers Database API",
|
||||||
|
"description": "An API for the 17th Rangers Database",
|
||||||
|
"contact": {
|
||||||
|
"email": "indigo@indigofox.dev",
|
||||||
|
"name": "Indigo Fox"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"name": "MIT",
|
||||||
|
"url": "https://opensource.org/license/mit/"
|
||||||
|
},
|
||||||
|
"version": "0.0.1"
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:3001/api",
|
||||||
|
"description": "Development"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://indigofox.dev:9230/api",
|
||||||
|
"description": "Production"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "members",
|
||||||
|
"description": "Operations on users/members"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ranks",
|
||||||
|
"description": "Rank information & related categories"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "awards",
|
||||||
|
"description": "Badges & ribbons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "courses",
|
||||||
|
"description": "Training courses"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "courseEvents",
|
||||||
|
"description": "Instances of trainings held for specific courses"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "member statuses",
|
||||||
|
"description": "Member status indicating active, inactive, company membership, etc."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"/members": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"members"
|
||||||
|
],
|
||||||
|
"summary": "Get all members",
|
||||||
|
"description": "Returns a list of all members",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "A list of members",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Member"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#/components/responses/Unauthorized"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Unexpected error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/responses/InternalServerError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"members"
|
||||||
|
],
|
||||||
|
"summary": "Create a member",
|
||||||
|
"description": "Creates a new member",
|
||||||
|
"requestBody": {
|
||||||
|
"description": "Member object that needs to be added",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/MemberPut"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Member created",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Member"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid input",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/responses/ClientInputError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#/components/responses/Unauthorized"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Unexpected error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/responses/InternalServerError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/members/{memberId}": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"members"
|
||||||
|
],
|
||||||
|
"summary": "Get a member by ID",
|
||||||
|
"description": "Returns a single member",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "memberId",
|
||||||
|
"in": "path",
|
||||||
|
"description": "ID of member to return",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Member found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Member"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#/components/responses/Unauthorized"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/components/responses/NotFound"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Unexpected error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/responses/InternalServerError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"tags": [
|
||||||
|
"members"
|
||||||
|
],
|
||||||
|
"summary": "Update a member by ID",
|
||||||
|
"description": "Updates a member",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "memberId",
|
||||||
|
"in": "path",
|
||||||
|
"description": "ID of member to update",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"description": "Member object that needs to be updated",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/MemberPut"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Member updated",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Member"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid input",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/responses/ClientInputError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#/components/responses/Unauthorized"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/components/responses/NotFound"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Unexpected error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/responses/InternalServerError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/training-reports": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"training-reports"
|
||||||
|
],
|
||||||
|
"summary": "Get all training reports",
|
||||||
|
"description": "Returns a list of all training reports",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "A list of training reports",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/TrainingReport"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#/components/responses/Unauthorized"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Unexpected error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/responses/InternalServerError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"training-reports"
|
||||||
|
],
|
||||||
|
"summary": "Create a training report",
|
||||||
|
"description": "Creates a new training report",
|
||||||
|
"requestBody": {
|
||||||
|
"$ref": "#/components/requestBodies/CourseEventTrainingReport",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Training report created",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TrainingReport"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid input",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/responses/ClientInputError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#/components/responses/Unauthorized"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Unexpected error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/responses/InternalServerError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"Member": {
|
||||||
|
"$ref": "schemas/member.json"
|
||||||
|
},
|
||||||
|
"MemberPut": {
|
||||||
|
"$ref": "schemas/memberPut.json"
|
||||||
|
},
|
||||||
|
"Rank": {
|
||||||
|
"$ref": "schemas/rank.json"
|
||||||
|
},
|
||||||
|
"Award": {
|
||||||
|
"$ref": "schemas/award.json"
|
||||||
|
},
|
||||||
|
"AwardAction": {
|
||||||
|
"$ref": "schemas/awardAction.json"
|
||||||
|
},
|
||||||
|
"CourseEvent": {
|
||||||
|
"$ref": "schemas/courseEvent.json"
|
||||||
|
},
|
||||||
|
"Error": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"message"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"description": "A human readable error message",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"requestBodies": {
|
||||||
|
"CourseEventTrainingReport": {
|
||||||
|
"$ref": "requestBodies/courseEventTrainingReport.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"Unauthorized": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"ClientInputError": {
|
||||||
|
"description": "Client Input Error"
|
||||||
|
},
|
||||||
|
"NotFound": {
|
||||||
|
"description": "Not Found"
|
||||||
|
},
|
||||||
|
"InternalServerError": {
|
||||||
|
"description": "Internal Server Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securitySchemes": {
|
||||||
|
"bearerAuth": {
|
||||||
|
"type": "http",
|
||||||
|
"scheme": "bearer",
|
||||||
|
"bearerFormat": "plain-text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1565
api/openapi.yaml
Normal file
1565
api/openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
1564
api/package-lock.json
generated
Normal file
1564
api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
api/package.json
Normal file
26
api/package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"dev": "nodemon --legacy-watch -e js,json,yaml index.js",
|
||||||
|
"prod": "node index.js"
|
||||||
|
},
|
||||||
|
"author": "IndigoFox",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.3.4",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"express-bearer-token": "^2.4.0",
|
||||||
|
"express-jwt": "^8.4.1",
|
||||||
|
"mysql2": "^3.2.0",
|
||||||
|
"nodemon": "^2.0.22",
|
||||||
|
"sequelize": "^6.29.3",
|
||||||
|
"swagger-ui-express": "^4.6.2",
|
||||||
|
"yamljs": "^0.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
134
defaults/ranks.json
Normal file
134
defaults/ranks.json
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Recruit",
|
||||||
|
"category": "Enlisted",
|
||||||
|
"sort_id": 22,
|
||||||
|
"image_url": "https://i.imgur.com/UE1Zs6g.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Private",
|
||||||
|
"category": "Enlisted",
|
||||||
|
"sort_id": 21,
|
||||||
|
"image_url": "http://i.imgur.com/Wh4nYns.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Private First Class",
|
||||||
|
"category": "Enlisted",
|
||||||
|
"sort_id": 20,
|
||||||
|
"image_url": "http://i.imgur.com/9V9PBDi.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Specialist",
|
||||||
|
"category": "Enlisted",
|
||||||
|
"sort_id": 19,
|
||||||
|
"image_url": "http://i.imgur.com/jEEuKKB.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Corporal",
|
||||||
|
"category": "NCO",
|
||||||
|
"sort_id": 18,
|
||||||
|
"image_url": "http://i.imgur.com/nfZrieG.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sergeant",
|
||||||
|
"category": "NCO",
|
||||||
|
"sort_id": 17,
|
||||||
|
"image_url": "http://i.imgur.com/hfGy0ZZ.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Staff Sergeant",
|
||||||
|
"category": "NCO",
|
||||||
|
"sort_id": 16,
|
||||||
|
"image_url": "http://i.imgur.com/ZVg95ep.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sergeant 1st Class",
|
||||||
|
"category": "NCO",
|
||||||
|
"sort_id": 15,
|
||||||
|
"image_url": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Master Sergeant",
|
||||||
|
"category": "NCO",
|
||||||
|
"sort_id": 14,
|
||||||
|
"image_url": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "1st Sergeant",
|
||||||
|
"category": "NCO",
|
||||||
|
"sort_id": 13,
|
||||||
|
"image_url": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sergeant Major",
|
||||||
|
"category": "NCO",
|
||||||
|
"sort_id": 12,
|
||||||
|
"image_url": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Warrant Officer 1",
|
||||||
|
"category": "Enlisted",
|
||||||
|
"sort_id": 11,
|
||||||
|
"image_url": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chief Warrant Officer 2",
|
||||||
|
"category": "Enlisted",
|
||||||
|
"sort_id": 10,
|
||||||
|
"image_url": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chief Warrant Officer 3",
|
||||||
|
"category": "NCO",
|
||||||
|
"sort_id": 9,
|
||||||
|
"image_url": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chief Warrant Officer 4",
|
||||||
|
"category": "NCO",
|
||||||
|
"sort_id": 8,
|
||||||
|
"image_url": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chief Warrant Officer 5",
|
||||||
|
"category": "NCO",
|
||||||
|
"sort_id": 7,
|
||||||
|
"image_url": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "2nd Lieutenant",
|
||||||
|
"category": "Officer",
|
||||||
|
"sort_id": 6,
|
||||||
|
"image_url": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "1st Lieutenant",
|
||||||
|
"category": "Officer",
|
||||||
|
"sort_id": 5,
|
||||||
|
"image_url": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Captain",
|
||||||
|
"category": "Officer",
|
||||||
|
"sort_id": 4,
|
||||||
|
"image_url": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Major",
|
||||||
|
"category": "Officer",
|
||||||
|
"sort_id": 3,
|
||||||
|
"image_url": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lieutenant Colonel",
|
||||||
|
"category": "Officer",
|
||||||
|
"sort_id": 2,
|
||||||
|
"image_url": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Staff",
|
||||||
|
"category": "",
|
||||||
|
"sort_id": 1,
|
||||||
|
"image_url": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
63
docker-compose.yaml
Normal file
63
docker-compose.yaml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
name: 17thrangers
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
env_file: .env
|
||||||
|
image: mysql
|
||||||
|
# NOTE: use of "mysql_native_password" is not recommended: https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html#upgrade-caching-sha2-password
|
||||||
|
# (this is just an example, not intended to be a production configuration)
|
||||||
|
command: --default-authentication-plugin=mysql_native_password
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- ./mysql:/var/lib/mysql
|
||||||
|
ports:
|
||||||
|
- 12730:3306
|
||||||
|
|
||||||
|
api:
|
||||||
|
env_file: .env
|
||||||
|
build:
|
||||||
|
context: ./api
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
# use bind mount - make sure you go to ./api and run npm i first
|
||||||
|
volumes:
|
||||||
|
- ./api:/app
|
||||||
|
command: npm run dev
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
|
||||||
|
MYSQL_DATABASE: ${DB_DATABASE}
|
||||||
|
MYSQL_USER: ${DB_USER}
|
||||||
|
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
# get self-signed cert
|
||||||
|
nginx:
|
||||||
|
env_file: .env
|
||||||
|
image: nginx:latest
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 9229:9229
|
||||||
|
- 9230:9230
|
||||||
|
volumes:
|
||||||
|
- ./nginx/conf/:/etc/nginx/conf.d
|
||||||
|
# - ./certbot/www:/var/www/certbot/:ro
|
||||||
|
# - ./certbot/conf/:/etc/nginx/ssl/:ro
|
||||||
|
|
||||||
|
# my implementation, already have another primary instance
|
||||||
|
- /home/certbot/www:/var/www/certbot/:ro
|
||||||
|
- /home/certbot/conf/:/etc/nginx/ssl/:ro
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
|
||||||
|
certbot:
|
||||||
|
image: certbot/certbot:latest
|
||||||
|
volumes:
|
||||||
|
- ./certbot/www/:/var/www/certbot/:rw
|
||||||
|
- ./certbot/conf/:/etc/letsencrypt/:rw
|
||||||
1
mysql/.gitkeep
Normal file
1
mysql/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{\rtf1}
|
||||||
40
nginx/conf/api.conf
Normal file
40
nginx/conf/api.conf
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
server {
|
||||||
|
listen 9229;
|
||||||
|
listen [::]:9229;
|
||||||
|
|
||||||
|
server_name example.org;
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# redirect to 9230
|
||||||
|
return 301 https://example.org:9230$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 9230 default_server ssl http2;
|
||||||
|
listen [::]:9230 ssl http2;
|
||||||
|
|
||||||
|
server_name example.org;
|
||||||
|
ssl_certificate /etc/nginx/ssl/live/example.org/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/live/example.org/privkey.pem;
|
||||||
|
location / {
|
||||||
|
# send to api container
|
||||||
|
proxy_pass http://api:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_set_header X-Forwarded-Server $host;
|
||||||
|
proxy_set_header X-Forwarded-Uri $request_uri;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
}
|
||||||
9
nginx/conf/certmgmt.txt
Normal file
9
nginx/conf/certmgmt.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# DRY RUN - ensure certificates CAN be created
|
||||||
|
docker compose run --rm certbot certonly --webroot --webroot-path /var/www/certbot/ --dry-run -d example.org
|
||||||
|
|
||||||
|
# PROD RUN - generate certificates for the provided site
|
||||||
|
docker compose run --rm certbot certonly --webroot --webroot-path /var/www/certbot/ -d example.org
|
||||||
|
|
||||||
|
|
||||||
|
# RENEW CERTIFICATES - run every 3 months
|
||||||
|
docker compose run --rm certbot renew
|
||||||
Reference in New Issue
Block a user