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:
2023-03-28 00:08:50 -07:00
parent 2d6d44b89f
commit 9f2473801c
82 changed files with 13974 additions and 1 deletions

14
17th-web/.eslintrc.cjs Normal file
View 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
View 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?

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

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

35
17th-web/README.md Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

37
17th-web/package.json Normal file
View 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"
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

95
17th-web/src/App.vue Normal file
View 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>

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

View 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

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

View File

@@ -0,0 +1,43 @@
<script setup>
defineProps({
msg: {
type: String,
required: true
}
})
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve 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>

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

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

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

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

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

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

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

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

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

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

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

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

View 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

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

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

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

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

@@ -0,0 +1,7 @@
#app {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}

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

View File

@@ -0,0 +1,9 @@
<script setup>
import TheWelcome from '../components/TheWelcome.vue'
</script>
<template>
<main>
<TheWelcome />
</main>
</template>

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

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

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

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

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

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

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

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

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

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