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:
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)),
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user