Compare commits

..

5 Commits
master ... dev

29 changed files with 1251 additions and 75 deletions

View File

@ -0,0 +1,2 @@
VITE_API_BASE_URL=/api
#VITE_API_BASE_URL=http://localhost:8008/api

View File

@ -18,6 +18,7 @@
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-draggable-plus": "^0.5.3",
"vue-i18n": "^9.13.1", "vue-i18n": "^9.13.1",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
}, },
@ -1847,6 +1848,12 @@
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
}, },
"node_modules/@types/sortablejs": {
"version": "1.15.8",
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.8.tgz",
"integrity": "sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==",
"license": "MIT"
},
"node_modules/@types/tough-cookie": { "node_modules/@types/tough-cookie": {
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
@ -1855,17 +1862,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.13.0", "version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.13.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
"integrity": "sha512-FX1X6AF0w8MdVFLSdqwqN/me2hyhuQg4ykN6ZpVhh1ij/80pTvDKclX1sZB9iqex8SjQfVhwMKs3JtnnMLzG9w==", "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.13.0", "@typescript-eslint/scope-manager": "7.18.0",
"@typescript-eslint/type-utils": "7.13.0", "@typescript-eslint/type-utils": "7.18.0",
"@typescript-eslint/utils": "7.13.0", "@typescript-eslint/utils": "7.18.0",
"@typescript-eslint/visitor-keys": "7.13.0", "@typescript-eslint/visitor-keys": "7.18.0",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.3.1", "ignore": "^5.3.1",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@ -1889,16 +1896,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "7.13.0", "version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.13.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz",
"integrity": "sha512-EjMfl69KOS9awXXe83iRN7oIEXy9yYdqWfqdrFAYAAr6syP8eLEFI7ZE4939antx2mNgPRW/o1ybm2SFYkbTVA==", "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "7.13.0", "@typescript-eslint/scope-manager": "7.18.0",
"@typescript-eslint/types": "7.13.0", "@typescript-eslint/types": "7.18.0",
"@typescript-eslint/typescript-estree": "7.13.0", "@typescript-eslint/typescript-estree": "7.18.0",
"@typescript-eslint/visitor-keys": "7.13.0", "@typescript-eslint/visitor-keys": "7.18.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@ -1918,14 +1925,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "7.13.0", "version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
"integrity": "sha512-ZrMCe1R6a01T94ilV13egvcnvVJ1pxShkE0+NDjDzH4nvG1wXpwsVI5bZCvE7AEDH1mXEx5tJSVR68bLgG7Dng==", "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.13.0", "@typescript-eslint/types": "7.18.0",
"@typescript-eslint/visitor-keys": "7.13.0" "@typescript-eslint/visitor-keys": "7.18.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@ -1936,14 +1943,14 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "7.13.0", "version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.13.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz",
"integrity": "sha512-xMEtMzxq9eRkZy48XuxlBFzpVMDurUAfDu5Rz16GouAtXm0TaAoTFzqWUFPPuQYXI/CDaH/Bgx/fk/84t/Bc9A==", "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "7.13.0", "@typescript-eslint/typescript-estree": "7.18.0",
"@typescript-eslint/utils": "7.13.0", "@typescript-eslint/utils": "7.18.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.3.0" "ts-api-utils": "^1.3.0"
}, },
@ -1964,9 +1971,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "7.13.0", "version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
"integrity": "sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==", "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -1978,14 +1985,14 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "7.13.0", "version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
"integrity": "sha512-cAvBvUoobaoIcoqox1YatXOnSl3gx92rCZoMRPzMNisDiM12siGilSM4+dJAekuuHTibI2hVC2fYK79iSFvWjw==", "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.13.0", "@typescript-eslint/types": "7.18.0",
"@typescript-eslint/visitor-keys": "7.13.0", "@typescript-eslint/visitor-keys": "7.18.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@ -2007,16 +2014,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "7.13.0", "version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.13.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
"integrity": "sha512-jceD8RgdKORVnB4Y6BqasfIkFhl4pajB1wVxrF4akxD2QPM8GNYjgGwEzYS+437ewlqqrg7Dw+6dhdpjMpeBFQ==", "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.13.0", "@typescript-eslint/scope-manager": "7.18.0",
"@typescript-eslint/types": "7.13.0", "@typescript-eslint/types": "7.18.0",
"@typescript-eslint/typescript-estree": "7.13.0" "@typescript-eslint/typescript-estree": "7.18.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@ -2030,13 +2037,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "7.13.0", "version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
"integrity": "sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw==", "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.13.0", "@typescript-eslint/types": "7.18.0",
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
}, },
"engines": { "engines": {
@ -2753,9 +2760,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.7.3", "version": "1.7.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz",
"integrity": "sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==", "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
@ -6445,6 +6452,23 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/vue-draggable-plus": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/vue-draggable-plus/-/vue-draggable-plus-0.5.3.tgz",
"integrity": "sha512-dwKDzZ8io3y7k2iuIwVwiGrdiq5C0S7Et7nt5Gz5KjpBS9MtZGFP+L4FJPWGSLYleOT8HmVuCXTZYjGet7wC0g==",
"license": "MIT",
"dependencies": {
"@types/sortablejs": "^1.15.8"
},
"peerDependencies": {
"@types/sortablejs": "^1.15.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/vue-eslint-parser": { "node_modules/vue-eslint-parser": {
"version": "9.4.3", "version": "9.4.3",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",

View File

@ -23,6 +23,7 @@
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-draggable-plus": "^0.5.3",
"vue-i18n": "^9.13.1", "vue-i18n": "^9.13.1",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
}, },

View File

@ -2,7 +2,7 @@
import { provide, ref } from 'vue'; import { provide, ref } from 'vue';
import { RouterView } from 'vue-router'; import { RouterView } from 'vue-router';
import { infoModalShowFnKey } from './services/UtilService'; import { infoModalShowFnKey, navbarKey } from './services/UtilService';
import NavBar from '@/components/blocks/NavBar.vue'; import NavBar from '@/components/blocks/NavBar.vue';
import GenericInfoModal from '@/components/modals/GenericInfoModal.vue'; import GenericInfoModal from '@/components/modals/GenericInfoModal.vue';
@ -30,11 +30,13 @@ function showInfoModal( title: string, text: string ): void {
provide( infoModalShowFnKey, showInfoModal ); provide( infoModalShowFnKey, showInfoModal );
const navbar = ref<InstanceType<typeof NavBar> | undefined>(undefined);
provide( navbarKey, navbar);
</script> </script>
<template> <template>
<div class="vh-100 overflow-y-scroll overflow-x-hidden"> <div class="vh-100 overflow-y-scroll overflow-x-hidden">
<NavBar :userLoading="userLoading" /> <NavBar ref="navbar" :userLoading="userLoading" />
<RouterView /> <RouterView />

View File

@ -5,3 +5,7 @@
.pointer { .pointer {
cursor: pointer; cursor: pointer;
} }
.cursor-move{
cursor: move;
}

View File

@ -46,11 +46,15 @@ $body-secondary-bg: $light-accented;
$dropdown-link-hover-bg: $dark-accented; $dropdown-link-hover-bg: $dark-accented;
$modal-fade-transform: scale(.75); $modal-fade-transform: scale(.75);
$breadcrumb-divider: quote(">");
// 3. Include remainder of required Bootstrap stylesheets (including any separate color mode stylesheets) // 3. Include remainder of required Bootstrap stylesheets (including any separate color mode stylesheets)
@import "bootstrap/scss/variables"; @import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark"; @import "bootstrap/scss/variables-dark";
$form-file-button-bg: var(--#{$prefix}secondary-bg);
$form-file-button-hover-bg: var(--#{$prefix}tertiary-bg);
/* Bootstrap Color Map adjustments */ /* Bootstrap Color Map adjustments */
$custom-colors: ( $custom-colors: (
@ -89,6 +93,36 @@ $utilities: map-merge(
), ),
), ),
), ),
"rounded-bottom-end": (
property: border-bottom-right-radius,
class: rounded-bottom-end,
values: (
null: var(--#{$prefix}border-radius),
0: 0,
1: var(--#{$prefix}border-radius-sm),
2: var(--#{$prefix}border-radius),
3: var(--#{$prefix}border-radius-lg),
4: var(--#{$prefix}border-radius-xl),
5: var(--#{$prefix}border-radius-xxl),
circle: 50%,
pill: var(--#{$prefix}border-radius-pill)
)
),
"rounded-top-end": (
property: border-top-right-radius,
class: rounded-top-end,
values: (
null: var(--#{$prefix}border-radius),
0: 0,
1: var(--#{$prefix}border-radius-sm),
2: var(--#{$prefix}border-radius),
3: var(--#{$prefix}border-radius-lg),
4: var(--#{$prefix}border-radius-xl),
5: var(--#{$prefix}border-radius-xxl),
circle: 50%,
pill: var(--#{$prefix}border-radius-pill)
)
),
), ),
); );

View File

@ -0,0 +1,97 @@
<script setup lang="ts">
import type { BoardEntry } from '@/models/board/BoardEntry';
import { computed } from 'vue';
const QUESTION_TYPE_SIMPLE_TEXT_ID = 1;
const QUESTION_TYPE_IMAGE_ID = 2;
const props = defineProps<{
boardEntry: BoardEntry,
selectedQuestionIndex: number,
isAnswerShown: boolean,
}>();
const emit = defineEmits<{
questionSelected: [questionIndex: number],
}>();
const question = computed( () => {
return props.boardEntry.questions[props.selectedQuestionIndex ?? 0];
});
function selectQuestionIndex( qIndex: number ) {
emit( "questionSelected", qIndex );
}
</script>
<template>
<div class="h-100 d-flex flex-column">
<div class="row h-100">
<div class="col h-100 mx-3 overflow-y-auto">
<div class="ratio ratio-16x9">
<div class="w-100 h-100 d-flex justify-content-center align-items-center">
<span v-if="boardEntry.questions.length === 0" class="fs-1">
No Question to show
</span>
<span v-else-if="question.questionType.id === QUESTION_TYPE_SIMPLE_TEXT_ID" class="text-center preserve-breaks" :style="`font-size: ${question.fontScaling}em`">
{{ question.text }}
</span>
<template v-else-if="question.questionType.id === QUESTION_TYPE_IMAGE_ID">
<div class="d-flex flex-column justify-content-center align-items-center h-100 w-100">
<span class="text-center preserve-breaks" :style="`font-size: ${question.fontScaling}em`">
{{ question.text }}
</span>
<div class="h-75 w-100 d-flex justify-content-center align-items-center">
<img
v-if="question.image"
:src="question.image"
alt="User uploaded - No caption available"
class="h-100 w-100 object-contain"
>
</div>
</div>
</template>
<!-- Category Name -->
<div class="position-absolute top-0 start-0 mt-2">
<span class="fs-2">
{{ boardEntry.category.name }}
</span>
</div>
<!-- Points -->
<div class="position-absolute bottom-0 end-0">
<span class="fs-2">
{{ boardEntry.points }}
</span>
</div>
<!-- Answer -->
<div v-if="isAnswerShown" class="position-absolute bottom-0 start-50 translate-middle-x mb-2">
<div class="bg-primary p-2 rounded bg-opacity-50 fs-4 text-center">
Answer:<br>{{ boardEntry.answer.text }}
</div>
</div>
</div>
</div>
</div>
</div>
<div v-if=" props.boardEntry.questions.length > 1 " class="position-absolute bottom-0 start-0 mb-3 ms-3">
<template v-for="( question, questionIndex) in props.boardEntry.questions" :key="question.id">
<button class="btn me-2"
:class="[
{ 'btn-primary': props.selectedQuestionIndex === questionIndex },
{ 'btn-outline-primary': props.selectedQuestionIndex !== questionIndex }
]"
:disabled="props.selectedQuestionIndex === questionIndex"
@click="selectQuestionIndex( questionIndex )">
{{ questionIndex + 1 }}
</button>
</template>
</div>
</div>
</template>
<style lang="css" scoped>
.object-contain{
object-fit: contain;
}
</style>

View File

@ -1,6 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
const router = useRouter();
const boards = ref([{ const boards = ref([{
id: 1, id: 1,
@ -15,6 +19,10 @@ const boards = ref([{
id: 4, id: 4,
boardName: "Mocka Board 2", boardName: "Mocka Board 2",
}]); }]);
function createNewBoard(){
router.push( { name: 'create' } );
}
</script> </script>
<template> <template>
@ -44,5 +52,11 @@ const boards = ref([{
</div> </div>
</div> </div>
</template> </template>
<div class="col-4 mb-3">
<button class="btn btn-outline-primary w-100 h-100 d-flex flex-column justify-content-center" @click="createNewBoard">
Create new Board
<FontAwesomeIcon :icon="['fas', 'plus']" size="2x"/>
</button>
</div>
</div> </div>
</template> </template>

View File

@ -0,0 +1,68 @@
<script setup lang="ts">
import type { Board } from '@/models/board/Board';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps<{
board: Board,
}>();
const emit = defineEmits<{
categorySelected: [index: number],
boardEntrySelected: [cIndex: number, bEIndex: number],
}>();
function categorySelected(cIndex: number){
emit("categorySelected", cIndex);
}
function boardEntrySelected(cIndex: number, bEIndex: number){
emit("boardEntrySelected", cIndex, bEIndex);
}
</script>
<template>
<div class="mx-3 h-100 d-flex flex-column">
<div class="row">
<div class="col text-center p-3">
<h2>
{{ board.name }}
</h2>
</div>
</div>
<div class="row h-100">
<template v-for="(category, categoryIndex) in props.board.categories " :key="category.name">
<div class="col pb-2">
<div class="d-flex flex-column h-100">
<button class="flex-fill board-card-max-height card bg-primary w-100 my-1" @click="categorySelected(categoryIndex)" :title="board.categories[categoryIndex].description">
<div class="card-body d-flex align-items-center justify-content-center">
{{ category.name }}
</div>
</button>
<template v-for=" (boardEntry, boardEntryIndex) in category.boardEntries " :key="boardEntry.name">
<button class="flex-fill board-card-max-height card bg-body-secondary w-100 my-1" @click="boardEntrySelected(categoryIndex, boardEntryIndex)">
<div class="card-body d-flex align-items-center justify-content-center">
<template v-if="board.pointsAreTitle">
{{ boardEntry.points }}
</template>
<template v-else>
{{ boardEntry.name }}
</template>
</div>
</button>
</template>
</div>
</div>
</template>
</div>
</div>
</template>
<style lang="css" scoped>
.board-card-max-height{
max-height: 20%;
}
</style>

View File

@ -0,0 +1,93 @@
<script setup lang="ts">
import type { Board } from '@/models/board/Board';
import EditCategoryPanel from '@/components/blocks/EditCategoryPanel.vue';
import EditBoardPanel from '@/components/blocks/EditBoardPanel.vue';
import EditBoardEntryPanel from '@/components/blocks/EditBoardEntryPanel.vue';
import EditQuestionPanel from './EditQuestionPanel.vue';
const props = defineProps<{
categoryIndex: number | null,
boardEntryIndex: number | null,
questionIndex: number | null,
}>()
const emit = defineEmits<{
editBoard: [],
editCategory: [cIndex: number],
editBoardEntry: [cIndex: number, bEIndex: number],
editQuestion: [cIndex: number, bEIndex: number, qIndex: number],
}>()
const board = defineModel<Board>( { required: true } );
function editBoard(){
emit("editBoard");
}
function editCategory(cIndex: number){
emit("editCategory", cIndex);
}
function editBoardEntry(cIndex: number, bEIndex: number){
emit("editBoardEntry", cIndex, bEIndex);
}
function editQuestion(cIndex: number, bEIndex: number, qIndex: number){
emit("editQuestion", cIndex, bEIndex, qIndex);
}
</script>
<template>
<div class="d-flex flex-column h-100">
<div class="flex-grow-1 overflow-y-auto p-2">
<EditBoardPanel
v-if="props.categoryIndex === null && props.boardEntryIndex === null && props.questionIndex === null"
v-model="board"
@editCategory="editCategory"
/>
<EditCategoryPanel
v-else-if="props.categoryIndex !== null && props.boardEntryIndex === null && props.questionIndex === null"
v-model="board"
:categoryIndex="props.categoryIndex"
@editBoard="editBoard"
@editBoardEntry="editBoardEntry"
/>
<EditBoardEntryPanel
v-else-if="props.categoryIndex !== null && props.boardEntryIndex !== null && props.questionIndex === null"
v-model="board" :categoryIndex="props.categoryIndex"
:boardEntryIndex="props.boardEntryIndex"
@editQuestion="editQuestion"
@editCategory="editCategory"
@editBoard="editBoard"
/>
<EditQuestionPanel
v-else-if="props.categoryIndex !== null && props.boardEntryIndex !== null && props.questionIndex !== null"
v-model="board"
:categoryIndex="props.categoryIndex"
:boardEntryIndex="props.boardEntryIndex"
:questionIndex="props.questionIndex"
@editBoardEntry="editBoardEntry"
@editCategory="editCategory"
@editBoard="editBoard"
/>
</div>
<div class="border-top border-2 border-primary p-2">
<div class="d-flex align-items-center">
<div class="flex-grow-1 me-1">
<button class="btn btn-primary w-100">
Save
</button>
</div>
<div class="flex-grow-1 ms-1">
<button class="btn btn-danger w-100">
Exit
</button>
</div>
</div>
</div>
</div>
</template>
<style lang="css" scoped>
.draggable-ghost {
opacity: .75;
}
</style>

View File

@ -0,0 +1,201 @@
<script setup lang="ts">
import { computed, inject, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { VueDraggable } from 'vue-draggable-plus';
import type { Board } from '@/models/board/Board';
import { Question } from '@/models/board/Question';
import { QuestionType } from '@/models/board/QuestionType';
import { questionTypesKey } from '@/services/UtilService';
const { t } = useI18n();
const board = defineModel<Board>( { required: true } );
const props = defineProps<{
categoryIndex: number,
boardEntryIndex: number,
}>();
const questionTypes = inject(questionTypesKey);
const emit = defineEmits<{
editBoard: [],
editCategory: [categoryIndex: number],
editQuestion: [categoryIndex: number, boardEntryIndex: number, questionIndex: number],
}>()
const boardEntry = computed( () => {
return board.value.categories[props.categoryIndex].boardEntries[props.boardEntryIndex];
});
const newQuestionText = ref( '' );
const newQuestionType = ref<QuestionType | null>( (questionTypes ?? [null])[0] );
function addQuestion() {
if( boardEntry.value.questions.length >= 10 || newQuestionType.value === null) {
return;
}
const newQuestion = new Question(newQuestionText.value, newQuestionType.value, boardEntry.value );
boardEntry.value.questions.push( newQuestion );
}
function deleteQuestion( index: number ) {
boardEntry.value.questions.splice( index, 1 );
}
function moveQuestionUp( index: number ) {
if( index === 0 || boardEntry.value.questions.length <= 1 ) {
return;
}
const tmp = boardEntry.value.questions[index];
boardEntry.value.questions[index] = boardEntry.value.questions[index - 1];
boardEntry.value.questions[index - 1] = tmp;
}
function moveQuestionDown( index: number ) {
if( index === boardEntry.value.questions.length - 1 || boardEntry.value.questions.length <= 1 ) {
return;
}
const tmp = boardEntry.value.questions[index];
boardEntry.value.questions[index] = boardEntry.value.questions[index + 1];
boardEntry.value.questions[index + 1] = tmp;
}
function openBoard() {
emit("editBoard");
}
function openCategory(categoryIndex: number) {
emit("editCategory", categoryIndex);
}
function openQuestion(categoryIndex: number, boardEntryIndex: number, questionIndex: number) {
emit("editQuestion", categoryIndex, boardEntryIndex, questionIndex);
}
</script>
<template>
<div class="d-flex flex-column h-100">
<div class="flex-grow-1 px-2">
<div class="row">
<div class="col">
<div class="d-flex align-items-center mb-2 overflow-x-auto pb-2">
<button class="btn btn-sm btn-outline-primary" :title="t('common.back')" @click="openCategory(props.categoryIndex)">
<FontAwesomeIcon :icon="['fas', 'angle-left']"/>
</button>
<nav class="flex-grow-1 ms-2" aria-label="breadcrumb">
<ol class="breadcrumb mb-0 flex-nowrap">
<li class="breadcrumb-item text-truncate">
<a href="#" @click="openBoard">
{{ board.name }}
</a>
</li>
<li class="breadcrumb-item text-truncate">
<a href="#" @click="openCategory( props.categoryIndex )">
{{ board.categories[props.categoryIndex].name }}
</a>
</li>
<li class="breadcrumb-item active text-truncate" aria-current="page">
{{ boardEntry.name }}
</li>
</ol>
</nav>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="d-flex border-bottom border-3 border-primary align-items-center mb-2">
<h2 class="flex-grow-1 ms-2">{{ t( "board.boardentry.infos" ) }}</h2>
</div>
<label for="category-name">{{ t( 'board.boardentry.name' ) }}</label>
<input type="text" id="category-name" class="form-control mb-2" v-model="boardEntry.name" :placeholder="t( 'board.boardentry.name' )">
<label for="board-entry-points">{{ t( 'board.boardentry.points' ) }}</label>
<input type="text" id="board-entry-points" class="form-control" v-model="boardEntry.points" :placeholder="t( 'board.boardentry.points' )">
</div>
</div>
<div class="row mt-3">
<div class="col">
<h4>{{ t( "board.question.label", 2 ) }}</h4>
<template v-if=" boardEntry.questions.length === 0 ">
<p>
{{ t( "board.question.label", 0 ) }}
</p>
</template>
<template v-else>
<VueDraggable v-model="board.categories[props.categoryIndex].boardEntries" item-key="id" :animation="150" ghost-class="draggable-ghost"
tag="ul" class="list-group" handle=".drag-handle">
<template v-for="(question, questionIndex) of boardEntry.questions" :key="question.id">
<li class="list-group-item bg-body-secondary p-0">
<div class="d-flex justify-content-between w-100">
<div class="flex-grow-1 d-flex align-items-center text-truncate">
<div
class="drag-handle h-100 d-flex justify-content-center align-items-center cursor-move border-end border-1 bg-primary bg-opacity-10">
<FontAwesomeIcon :icon="['fas', 'grip-lines']" class="px-2" />
</div>
<div class="text-truncate">
<span class="ms-2">
{{ question.questionType.title }}
<span class="fw-light">
({{ question.text.length === 0 ? 'No Text yet' : question.text }})
</span>
</span>
</div>
</div>
<div class="d-flex">
<button class="btn btn-primary rounded-0" @click="openQuestion( props.categoryIndex, props.boardEntryIndex, questionIndex )">
<FontAwesomeIcon :icon="['fas', 'edit']" size="sm" />
</button>
<div class="d-flex flex-column justify-content-center align-items-center h-100" role="group"
aria-label="Vertical button group">
<button class="btn btn-primary rounded-0 py-0 lh-sm" @click="moveQuestionUp( questionIndex )"
:disabled="questionIndex === 0">
<FontAwesomeIcon :icon="['fas', 'angle-up']" size="xs" />
</button>
<button class="btn btn-primary rounded-0 py-0 lh-sm" @click="moveQuestionDown( questionIndex )"
:disabled="questionIndex === boardEntry.questions.length - 1">
<FontAwesomeIcon :icon="['fas', 'angle-down']" size="xs" />
</button>
</div>
<button class="btn btn-danger rounded-start-0 rounded-0"
:class="[{ 'rounded-bottom-end': questionIndex === boardEntry.questions.length - 1 }, { 'rounded-top-end': questionIndex === 0 }]"
@click="deleteQuestion( questionIndex )">
<FontAwesomeIcon :icon="['fas', 'trash']" size="sm" />
</button>
</div>
</div>
</li>
</template>
</VueDraggable>
</template>
<label class="mt-2" for="new-category-name">{{ t( "board.question.add" ) }}</label>
<div class="row mb-2">
<div class="col">
<select id="type-for-new-question" v-model="newQuestionType" class="form-select">
<template v-for="questionType in questionTypes" :key="questionType.id">
<option :value="questionType" :title="questionType.description">{{ questionType.title }}</option>
</template>
</select>
</div>
<div class="col-auto">
<button class="btn btn-primary" @click="addQuestion">
<FontAwesomeIcon :icon="['fas', 'plus']" />
</button>
</div>
</div>
<div class="row">
<div class="col">
<h4>{{ t('board.answer.label', 2) }}</h4>
<label for="answer-text">{{ t( 'board.answer.text' ) }}</label>
<textarea id="answer-text" v-model="boardEntry.answer.text" class="form-control mb-2"
:placeholder="t( 'board.answer.text' )"></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="css" scoped>
.draggable-ghost {
opacity: .75;
}
</style>

View File

@ -0,0 +1,131 @@
<script setup lang="ts">
import type { Board } from '@/models/board/Board';
import { Category } from '@/models/board/Category';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { VueDraggable } from 'vue-draggable-plus';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
const { t } = useI18n();
const emit = defineEmits<{
editCategory: [index: number],
}>()
const board = defineModel<Board>( { required: true } );
const newCategoryName = ref( '' );
function addCategory() {
if( board.value.categories.length >= 12 ) {
return;
}
const newCategory = new Category( newCategoryName.value, '', board.value );
board.value.categories.push( newCategory );
}
function deleteCategory( index: number ) {
board.value.categories.splice( index, 1 );
}
function moveCategoryUp( index: number ) {
if( index === 0 || board.value.categories.length <= 1 ) {
return;
}
const tmp = board.value.categories[index];
board.value.categories[index] = board.value.categories[index - 1];
board.value.categories[index - 1] = tmp;
}
function moveCategoryDown( index: number ) {
if( index === board.value.categories.length - 1 || board.value.categories.length <= 1 ) {
return;
}
const tmp = board.value.categories[index];
board.value.categories[index] = board.value.categories[index + 1];
board.value.categories[index + 1] = tmp;
}
function openCategory( index: number ) {
emit("editCategory", index);
}
</script>
<template>
<div class="d-flex flex-column h-100">
<div class="flex-grow-1 p-2">
<div class="row">
<div class="col">
<h2 class="border-bottom border-3 border-primary">{{ t( "board.infos" ) }}</h2>
<label for="board-name">{{ t( 'board.name' ) }}</label>
<input type="text" id="board-name" class="form-control" v-model="board.name" :placeholder="t( 'board.name' )">
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="points-for-title" v-model="board.pointsAreTitle">
<label for="points-for-title" class="form-check-label">{{ t( 'board.options.pointsForTitle' ) }}</label>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col">
<h4>{{ t( "board.category.label", 2 ) }}</h4>
<template v-if=" board.categories.length === 0 ">
{{ t( "board.category.label", 0 ) }}
</template>
<template v-else>
<VueDraggable v-model="board.categories" item-key="id" :animation="150" ghost-class="draggable-ghost"
tag="ul" class="list-group" handle=".drag-handle">
<template v-for="( category, categoryIndex) of board.categories" :key="category.id">
<li class="list-group-item bg-body-secondary p-0">
<div class="d-flex justify-content-between w-100">
<div class="flex-grow-1 d-flex align-items-center g-3 justify-content-start">
<div
class="drag-handle h-100 d-flex justify-content-center align-items-center cursor-move border-end border-1 bg-primary bg-opacity-10">
<FontAwesomeIcon :icon="['fas', 'grip-lines']" class="px-2" />
</div>
<input type="text" :id="`category-name-${categoryIndex}`" v-model="category.name"
class="form-control rounded-0 h-100">
</div>
<div class="d-flex">
<button class="btn btn-primary rounded-0" @click="openCategory( categoryIndex )">
<FontAwesomeIcon :icon="['fas', 'edit']" size="sm" />
</button>
<div class="d-flex flex-column justify-content-center align-items-center h-100" role="group"
aria-label="Vertical button group">
<button class="btn btn-primary rounded-0 py-0 lh-sm" @click="moveCategoryUp( categoryIndex )"
:disabled="categoryIndex === 0">
<FontAwesomeIcon :icon="['fas', 'angle-up']" size="xs" />
</button>
<button class="btn btn-primary rounded-0 py-0 lh-sm" @click="moveCategoryDown( categoryIndex )"
:disabled="categoryIndex === board.categories.length - 1">
<FontAwesomeIcon :icon="['fas', 'angle-down']" size="xs" />
</button>
</div>
<button class="btn btn-danger rounded-start-0 rounded-0"
:class="[{ 'rounded-bottom-end': categoryIndex === board.categories.length - 1 }, { 'rounded-top-end': categoryIndex === 0 }]"
@click="deleteCategory( categoryIndex )">
<FontAwesomeIcon :icon="['fas', 'trash']" size="sm" />
</button>
</div>
</div>
</li>
</template>
</VueDraggable>
</template>
<label class="mt-2" for="new-category-name">{{ t( "board.category.add" ) }}</label>
<div class="input-group mb-3">
<input type="text" id="new-category-name" class="form-control" v-model="newCategoryName"
:placeholder="t( 'board.category.name' )" @keyup.enter="addCategory">
<button class="btn btn-primary" @click="addCategory">
<FontAwesomeIcon :icon="['fas', 'plus']" />
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="css" scoped>
.draggable-ghost {
opacity: .75;
}
</style>

View File

@ -0,0 +1,148 @@
<script setup lang="ts">
import type { Board } from '@/models/board/Board';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { VueDraggable } from 'vue-draggable-plus';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { BoardEntry } from '@/models/board/BoardEntry';
import { Answer } from '@/models/board/Answer';
const { t } = useI18n();
const board = defineModel<Board>( { required: true } );
const props = defineProps<{
categoryIndex: number,
}>();
const emit = defineEmits<{
editBoardEntry: [categoryIndex: number, boardEntryIndex: number],
editBoard: [],
}>()
const newBoardEntryName = ref( '' );
function addBoardEntry() {
if( board.value.categories[props.categoryIndex].boardEntries.length >= 10 ) {
return;
}
const answer = new Answer('', undefined);
const newBoardEntry = new BoardEntry( newBoardEntryName.value, 0, board.value.categories[props.categoryIndex], answer, [] );
board.value.categories[props.categoryIndex].boardEntries.push( newBoardEntry );
}
function deleteBoardEntry( index: number ) {
board.value.categories[props.categoryIndex].boardEntries.splice( index, 1 );
}
function moveBoardEntryUp( index: number ) {
if( index === 0 || board.value.categories[props.categoryIndex].boardEntries.length <= 1 ) {
return;
}
const tmp = board.value.categories[props.categoryIndex].boardEntries[index];
board.value.categories[props.categoryIndex].boardEntries[index] = board.value.categories[props.categoryIndex].boardEntries[index - 1];
board.value.categories[props.categoryIndex].boardEntries[index - 1] = tmp;
}
function moveBoardEntryDown( index: number ) {
if( index === board.value.categories[props.categoryIndex].boardEntries.length - 1 || board.value.categories[props.categoryIndex].boardEntries.length <= 1 ) {
return;
}
const tmp = board.value.categories[props.categoryIndex].boardEntries[index];
board.value.categories[props.categoryIndex].boardEntries[index] = board.value.categories[props.categoryIndex].boardEntries[index + 1];
board.value.categories[props.categoryIndex].boardEntries[index + 1] = tmp;
}
function openBoardEntry( index: number ) {
emit("editBoardEntry", props.categoryIndex, index);
}
function openBoard() {
emit("editBoard");
}
</script>
<template>
<div class="d-flex flex-column h-100">
<div class="flex-grow-1 p-2">
<div class="row">
<div class="col">
<div class="d-flex border-bottom border-3 border-primary align-items-center mb-2">
<button class="btn btn-sm btn-outline-primary mb-1" :title="t('board.category.back')" @click="openBoard">
<FontAwesomeIcon :icon="['fas', 'angle-left']"/>
</button>
<h2 class="flex-grow-1 ms-2">{{ t( "board.category.infos" ) }}</h2>
</div>
<label for="category-name">{{ t( 'board.category.name' ) }}</label>
<input type="text" id="category-name" class="form-control mb-2" v-model="board.categories[props.categoryIndex].name" :placeholder="t( 'board.category.name' )">
<label for="category-description">{{ t( 'board.category.description' ) }}</label>
<textarea id="category-description" class="form-control" v-model="board.categories[props.categoryIndex].description" :placeholder="t( 'board.category.description' )">
</textarea>
</div>
</div>
<div class="row mt-3">
<div class="col">
<h4>{{ t( "board.boardentry.label", 2 ) }}</h4>
<template v-if=" board.categories[props.categoryIndex].boardEntries.length === 0 ">
<p>
{{ t( "board.boardentry.label", 0 ) }}
</p>
</template>
<template v-else>
<VueDraggable v-model="board.categories[props.categoryIndex].boardEntries" item-key="id" :animation="150" ghost-class="draggable-ghost"
tag="ul" class="list-group" handle=".drag-handle">
<template v-for="(boardEntry, boardEntryIndex) of board.categories[props.categoryIndex].boardEntries" :key="boardEntry.id">
<li class="list-group-item bg-body-secondary p-0">
<div class="d-flex justify-content-between w-100">
<div class="flex-grow-1 d-flex align-items-center g-3 justify-content-start">
<div
class="drag-handle h-100 d-flex justify-content-center align-items-center cursor-move border-end border-1 bg-primary bg-opacity-10">
<FontAwesomeIcon :icon="['fas', 'grip-lines']" class="px-2" />
</div>
<input type="text" :id="`category-name-${boardEntryIndex}`" v-model="boardEntry.name"
class="form-control rounded-0 h-100">
</div>
<div class="d-flex">
<button class="btn btn-primary rounded-0" @click="openBoardEntry( boardEntryIndex )">
<FontAwesomeIcon :icon="['fas', 'edit']" size="sm" />
</button>
<div class="d-flex flex-column justify-content-center align-items-center h-100" role="group"
aria-label="Vertical button group">
<button class="btn btn-primary rounded-0 py-0 lh-sm" @click="moveBoardEntryUp( boardEntryIndex )"
:disabled="boardEntryIndex === 0">
<FontAwesomeIcon :icon="['fas', 'angle-up']" size="xs" />
</button>
<button class="btn btn-primary rounded-0 py-0 lh-sm" @click="moveBoardEntryDown( boardEntryIndex )"
:disabled="boardEntryIndex === board.categories[props.categoryIndex].boardEntries.length - 1">
<FontAwesomeIcon :icon="['fas', 'angle-down']" size="xs" />
</button>
</div>
<button class="btn btn-danger rounded-start-0 rounded-0"
:class="[{ 'rounded-bottom-end': boardEntryIndex === board.categories[props.categoryIndex].boardEntries.length - 1 }, { 'rounded-top-end': boardEntryIndex === 0 }]"
@click="deleteBoardEntry( boardEntryIndex )">
<FontAwesomeIcon :icon="['fas', 'trash']" size="sm" />
</button>
</div>
</div>
</li>
</template>
</VueDraggable>
</template>
<label class="mt-2" for="new-category-name">{{ t( "board.boardentry.add" ) }}</label>
<div class="input-group mb-3">
<input type="text" id="new-category-name" class="form-control" v-model="newBoardEntryName"
:placeholder="t( 'board.boardentry.name' )" @keyup.enter="addBoardEntry">
<button class="btn btn-primary" @click="addBoardEntry">
<FontAwesomeIcon :icon="['fas', 'plus']" />
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="css" scoped>
.draggable-ghost {
opacity: .75;
}
</style>

View File

@ -0,0 +1,129 @@
<script setup lang="ts">
import type { Board } from '@/models/board/Board';
import { computed, inject, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { questionTypesKey } from '@/services/UtilService';
const QUESTION_TYPE_IMAGE_ID = 2;
const { t } = useI18n();
const board = defineModel<Board>( { required: true } );
const props = defineProps<{
categoryIndex: number,
boardEntryIndex: number,
questionIndex: number;
}>();
const questionTypes = inject( questionTypesKey );
const emit = defineEmits<{
editBoard: [],
editCategory: [categoryIndex: number],
editBoardEntry: [categoryIndex: number, boardEntryIndex: number],
}>();
const question = computed( () => {
return board.value.categories[props.categoryIndex].boardEntries[props.boardEntryIndex].questions[props.questionIndex];
} );
function openBoard() {
emit( "editBoard" );
}
function openCategory( categoryIndex: number ) {
emit( "editCategory", categoryIndex );
}
function openBoardEntry( categoryIndex: number, boardEntryIndex: number ) {
emit( "editBoardEntry", categoryIndex, boardEntryIndex );
}
const imageInput = ref<File | null>( null );
function newImageUploaded( event: Event ) {
const element = event.currentTarget as HTMLInputElement;
let files = element.files;
if( files === null || files.length === 0 ) {
return;
}
imageInput.value = files[0];
question.value.image = URL.createObjectURL(files[0]);
}
</script>
<template>
<div class="d-flex flex-column h-100">
<div class="flex-grow-1 px-2">
<div class="row">
<div class="col">
<div class="d-flex align-items-center mb-2 overflow-x-auto pb-2">
<button class="btn btn-sm btn-outline-primary" :title="t( 'common.back' )"
@click="openBoardEntry( props.categoryIndex, props.boardEntryIndex )">
<FontAwesomeIcon :icon="['fas', 'angle-left']" />
</button>
<nav class="flex-grow-1 ms-2" aria-label="breadcrumb">
<ol class="breadcrumb mb-0 flex-nowrap">
<li class="breadcrumb-item text-truncate">
<a href="#" @click="openBoard">
{{ board.name }}
</a>
</li>
<li class="breadcrumb-item text-truncate">
<a href="#" @click="openCategory( props.categoryIndex )">
{{ board.categories[props.categoryIndex].name }}
</a>
</li>
<li class="breadcrumb-item text-truncate">
<a href="#" @click="openBoardEntry( props.categoryIndex, props.boardEntryIndex )">
{{ board.categories[props.categoryIndex].boardEntries[props.boardEntryIndex].name }}
</a>
</li>
<li class="breadcrumb-item active text-truncate" style="max-width: 6em;" aria-current="page">
{{ question.text.length !== 0 ? question.text : question.questionType.title }}
</li>
</ol>
</nav>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="d-flex border-bottom border-3 border-primary align-items-center mb-2">
<h2 class="flex-grow-1 ms-2">{{ t( "board.question.infos" ) }}</h2>
</div>
<label for="question-text">{{ t( 'board.question.text' ) }}</label>
<textarea id="question-text" v-model="question.text" class="form-control mb-2"
:placeholder="t( 'board.question.text' )"></textarea>
<label for="question-font-size-input">{{ t( 'board.question.fontsize' ) }}</label>
<input type="number" id="question-font-size-input" v-model="question.fontScaling" class="form-control mb-2">
<label for="board-entry-points">{{ t( 'board.question.type' ) }}</label>
<select id="question-type" v-model="question.questionType" class="form-select mb-2" aria-label="Question Type">
<template v-for=" questionType in questionTypes " :key="questionType.id">
<option :value="questionType" :title="questionType.description">{{ questionType.title }}</option>
</template>
</select>
<template v-if=" question.questionType.id === QUESTION_TYPE_IMAGE_ID ">
<label for="question-image-input">{{ t( 'board.question.upload.image' ) }}</label>
<input
id="question-image-input"
type="file"
class="form-control mb-2"
@change="newImageUploaded"
accept="image/png, image/jpeg"
>
</template>
</div>
</div>
</div>
</div>
</template>
<style lang="css" scoped>
.draggable-ghost {
opacity: .75;
}
</style>

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { onMounted, ref } from 'vue';
import { RouterLink, useRoute, useRouter } from 'vue-router'; import { RouterLink, useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -8,6 +8,7 @@ import ThemeChanger from '@/components/blocks/ThemeChanger.vue';
import LocaleChanger from '@/components/blocks/LocaleChanger.vue'; import LocaleChanger from '@/components/blocks/LocaleChanger.vue';
import { useUserStore } from '@/stores/UserStore'; import { useUserStore } from '@/stores/UserStore';
import { authService } from '@/services/AuthService'; import { authService } from '@/services/AuthService';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
const navNames = { const navNames = {
HOME: "home", HOME: "home",
@ -21,6 +22,15 @@ const route = useRoute();
const userStore = useUserStore(); const userStore = useUserStore();
const navbar = ref<HTMLElement>();
defineExpose({
navElement: navbar
});
onMounted( () => {
console.log(typeof navbar.value);
})
const isActiveNav = ( navName: string ) => { const isActiveNav = ( navName: string ) => {
switch( navName ) { switch( navName ) {
case navNames.HOME: case navNames.HOME:
@ -54,10 +64,10 @@ userStore.userCheckPromise
</script> </script>
<template> <template>
<nav id="navbar-main" class="navbar navbar-expand-lg bg-dark-accented"> <nav ref="navbar" id="navbar-main" class="navbar navbar-expand-lg bg-dark-accented">
<div class="container px-5"> <div class="container px-5">
<div class="position-absolute start-0 top-50 translate-middle-y d-flex ms-3 gap-3"> <div class="position-absolute start-0 top-50 translate-middle-y d-flex ms-3 gap-3 z-2">
<ThemeChanger /> <ThemeChanger />
<LocaleChanger /> <LocaleChanger />
</div> </div>
@ -90,13 +100,13 @@ userStore.userCheckPromise
<div class="position-absolute end-0 top-50 translate-middle-y d-flex me-3 align-items-center"> <div class="position-absolute end-0 top-50 translate-middle-y d-flex me-3 align-items-center">
<template v-if=" userCheckLoading "> <template v-if=" userCheckLoading ">
<FontAwesomeIcon :icon="['fas', 'spinner']" spin />
</template> </template>
<template v-else-if=" userStore.loggedIn "> <template v-else-if=" userStore.loggedIn ">
<div class="dropdown-toggle pointer" data-bs-toggle="dropdown" aria-expanded="false"> <button class="dropdown-toggle pointer bg-dark py-1 px-2 btn" data-bs-toggle="dropdown" aria-expanded="false">
<img class="pfp-sizing rounded-circle border border-1 border-primary" :src="userStore.pfpSource" <img class="pfp-sizing rounded-circle border border-1 border-primary" :src="userStore.pfpSource"
alt="The Profile Pic of the user" /> alt="The Profile Pic of the user" />
</div> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li> <li>
<p class="dropdown-header fs-5 pt-0 text-primary fw-semibold">{{ userStore.getUserOutput }}</p> <p class="dropdown-header fs-5 pt-0 text-primary fw-semibold">{{ userStore.getUserOutput }}</p>

View File

@ -0,0 +1,101 @@
<script setup lang="ts">
import { computed, inject, provide, ref, type Ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { navbarKey, questionTypesKey } from '@/services/UtilService';
import { Board } from '@/models/board/Board';
import type NavBar from '@/components/blocks/NavBar.vue';
import BoardView from '@/components/blocks/BoardView.vue';
import CreatePanel from '@/components/blocks/CreatePanel.vue';
import { Category } from '@/models/board/Category';
import BoardEntryView from '../blocks/BoardEntryView.vue';
import { BoardEntry } from '@/models/board/BoardEntry';
import { Answer } from '@/models/board/Answer';
import { QuestionType } from '@/models/board/QuestionType';
const { t } = useI18n();
const navbar = inject<Ref<InstanceType<typeof NavBar> | null>>( navbarKey );
const navbarHeight = computed( () => {
return navbar?.value?.navElement?.clientHeight;
} );
const restHeight = computed( () => {
return { height: `calc(100vh - ${navbarHeight.value}px)` };
} );
const board = ref<Board>( new Board( "New Board" ) );
const board1 = ref<Board>( new Board( "New Board", [new Category( "Test1", "", board.value as Board ), new Category( "Test2", "", board.value as Board )] ) );
const answer = new Answer( '', undefined );
const newBoardEntry = new BoardEntry( "Test Entry 1", board1.value.categories[0] as Category, answer, [] );
board1.value.categories[0].boardEntries.push( newBoardEntry );
const categoryIndex = ref<number | null>( null );
const boardEntryIndex = ref<number | null>( null );
const questionIndex = ref<number | null>( null );
function showQuestion( cIndex: number, bEIndex: number, qIndex: number ) {
categoryIndex.value = cIndex;
boardEntryIndex.value = bEIndex;
questionIndex.value = qIndex;
}
function showBoardEntry( cIndex: number, bEIndex: number ) {
categoryIndex.value = cIndex;
boardEntryIndex.value = bEIndex;
questionIndex.value = null;
}
function showCategory( cIndex: number ) {
categoryIndex.value = cIndex;
boardEntryIndex.value = null;
questionIndex.value = null;
}
function showBoard() {
categoryIndex.value = null;
boardEntryIndex.value = null;
questionIndex.value = null;
}
const questionTypes = [
new QuestionType("Simple Text", "A simple question with just text", true, 1),
new QuestionType("Image Question", "A question with text and an image", true, 2),
new QuestionType("Audio Question", "A question with text and some audio", true, 3),
];
provide(questionTypesKey, questionTypes);
</script>
<template>
<div :style="restHeight">
<div class="row h-100">
<div class="col-9 h-100 pe-0">
<BoardEntryView
v-if=" categoryIndex !== null && boardEntryIndex !== null "
:boardEntry="( board1.categories[categoryIndex].boardEntries[boardEntryIndex] as BoardEntry )"
:selectedQuestionIndex="questionIndex ?? 0"
:isAnswerShown="true"
@questionSelected="(qIndex) => showQuestion(categoryIndex!, boardEntryIndex!, qIndex)"
/>
<BoardView
v-else
:board="( board1 as Board )"
@categorySelected="showCategory"
@boardEntrySelected="showBoardEntry" />
</div>
<div class="col-3 ps-0 h-100 overflow-auto border-start border-2 border-primary">
<CreatePanel
v-model="( board1 as Board )"
:categoryIndex="categoryIndex"
:boardEntryIndex="boardEntryIndex"
:questionIndex="questionIndex"
@editBoard="showBoard"
@editCategory="showCategory"
@editBoardEntry="showBoardEntry"
@editQuestion="showQuestion"
/>
</div>
</div>
</div>
</template>

View File

@ -52,6 +52,7 @@
"save": "Save", "save": "Save",
"saveAndExit": "Save and Exit" "saveAndExit": "Save and Exit"
}, },
"back": "Back",
"error": { "error": {
"generic": "Error" "generic": "Error"
} }
@ -68,6 +69,41 @@
"alreadyHostedGome": "Wanna create a board and host a game yourself?", "alreadyHostedGome": "Wanna create a board and host a game yourself?",
"textCode": "Wanna create a board and host a game yourself?" "textCode": "Wanna create a board and host a game yourself?"
}, },
"board": {
"label": "Board",
"name": "Board Name",
"infos": "Board Infos",
"options": {
"pointsForTitle": "Show points amount instead of entry name"
},
"category": {
"infos": "Category Infos",
"label": "No Category | Category | Categories | {count} Categories",
"name": "Category Name",
"description": "Category Description",
"add": "Add Category",
"back": "Back to Board"
},
"boardentry": {
"infos":"Entry Infos",
"points": "Point | Points | {count} Points",
"label": "No Entries | Entry | Entries | {count} Entries",
"add": "Add Entry",
"name": "Entry Name"
},
"question": {
"label": "No Question Layers | Question Layer | Question Layers | {count} Question Layers",
"infos": "Question Layer Infos",
"add": "Add Question Layer",
"text": "Question Text",
"type": "Question Type",
"fontsize": "Font Size",
"upload": {
"image": "Upload an image",
"audio": "Upload an audio file"
}
}
},
"theme": { "theme": {
"dark": { "dark": {
"name": "Dark" "name": "Dark"

View File

@ -9,7 +9,7 @@ import '@/assets/scss/customized_bootstrap.scss';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { library } from '@fortawesome/fontawesome-svg-core'; import { library } from '@fortawesome/fontawesome-svg-core';
import { faSun, faMoon, faCircleHalfStroke, faEdit, faPlay, faSpinner, faLanguage, faGlobe } from '@fortawesome/free-solid-svg-icons'; import { faSun, faMoon, faCircleHalfStroke, faEdit, faPlay, faSpinner, faLanguage, faGlobe, faPlus, faTrash, faGripLines, faAngleUp, faAngleDown, faAngleLeft } from '@fortawesome/free-solid-svg-icons';
import enMessages from './locales/en.json'; import enMessages from './locales/en.json';
import deMessages from './locales/de.json'; import deMessages from './locales/de.json';
@ -33,6 +33,12 @@ library.add(
faSpinner, faSpinner,
faLanguage, faLanguage,
faGlobe, faGlobe,
faPlus,
faTrash,
faGripLines,
faAngleUp,
faAngleDown,
faAngleLeft,
) )
const app = createApp( App ); const app = createApp( App );

View File

@ -0,0 +1,10 @@
import type { BoardEntry } from './BoardEntry';
export class Answer{
constructor(
public text: string,
public boardEntry: BoardEntry | undefined,
public image: URL | undefined = undefined,
public id: number | undefined = undefined,
){}
}

View File

@ -0,0 +1,12 @@
import type { User } from '../user/User';
import type { Category } from './Category';
export class Board{
constructor(
public name: string,
public categories: Array<Category> = [],
public pointsAreTitle: boolean = false,
public owner: User | undefined = undefined,
public id: number | undefined = undefined,
){}
}

View File

@ -0,0 +1,14 @@
import type { Answer } from './Answer';
import type { Category } from './Category';
import type { Question } from './Question';
export class BoardEntry{
constructor(
public name: string,
public category: Category,
public answer: Answer,
public questions: Array<Question>,
public points: number = 100,
public id: number | undefined = undefined,
){}
}

View File

@ -0,0 +1,12 @@
import type { Board } from './Board';
import type { BoardEntry } from './BoardEntry';
export class Category{
constructor(
public name: string,
public description: string,
public board: Board,
public boardEntries: Array<BoardEntry> = [],
public id: number | undefined = undefined,
){}
}

View File

@ -0,0 +1,13 @@
import type { BoardEntry } from './BoardEntry';
import type { QuestionType } from './QuestionType';
export class Question{
constructor(
public text: string,
public questionType: QuestionType,
public boardEntry: BoardEntry,
public fontScaling: number = 3,
public image: string | undefined = undefined,
public id: number | undefined = undefined,
) {}
}

View File

@ -0,0 +1,8 @@
export class QuestionType {
constructor(
public title: string,
public description: string,
public active: boolean,
public id: number | undefined = undefined,
){}
}

View File

@ -1,4 +1,5 @@
export type User = { export type User = {
username: string, username: string,
profilePictureFilename: string | undefined, profilePictureFilename: string | undefined,
id: number | undefined,
} }

View File

@ -1,11 +1,14 @@
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import { useUserStore } from '@/stores/UserStore';
import HomePage from '@/components/pages/HomePage.vue'; import HomePage from '@/components/pages/HomePage.vue';
import AboutPage from '@/components/pages/AboutPage.vue'; import AboutPage from '@/components/pages/AboutPage.vue';
import LoginPage from '@/components/pages/LoginPage.vue'; import LoginPage from '@/components/pages/LoginPage.vue';
import SignupPage from '@/components/pages/SignupPage.vue'; import SignupPage from '@/components/pages/SignupPage.vue';
import GamePage from '@/components/pages/GamePage.vue'; import GamePage from '@/components/pages/GamePage.vue';
import ProfilePage from '@/components/pages/ProfilePage.vue'; import ProfilePage from '@/components/pages/ProfilePage.vue';
import { useUserStore } from '@/stores/UserStore'; import CreatePage from '@/components/pages/CreatePage.vue';
const router = createRouter( { const router = createRouter( {
history: createWebHistory( import.meta.env.BASE_URL ), history: createWebHistory( import.meta.env.BASE_URL ),
@ -59,9 +62,9 @@ const router = createRouter( {
} }
}, },
{ {
path: '/board', path: '/create',
name: 'board', name: 'create',
component: ProfilePage, component: CreatePage,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
} }

View File

@ -1,3 +1,7 @@
import type { InjectionKey } from 'vue'; import type { InjectionKey, Ref } from 'vue';
import type NavBar from '@/components/blocks/NavBar.vue';
import type { QuestionType } from '@/models/board/QuestionType';
export const infoModalShowFnKey = Symbol() as InjectionKey<Function>; export const infoModalShowFnKey = Symbol() as InjectionKey<Function>;
export const navbarKey = Symbol() as InjectionKey<Ref<InstanceType<typeof NavBar> | undefined>>;
export const questionTypesKey = Symbol() as InjectionKey<Array<QuestionType>>;

View File

@ -5,25 +5,22 @@ import { authService } from '@/services/AuthService';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
export const useUserStore = defineStore( 'user', () => { export const useUserStore = defineStore( 'user', () => {
const username = ref( '' ); const loggedIn = computed( () => {
const profilePicture = ref<null | string>( null ); return user.value !== null;
const loggedIn = ref( false ); })
const user = ref<User | null>(null);
const getUserOutput = computed( () => `${username.value}` ); const getUserOutput = computed( () => `${user.value?.username}` );
const pfpSource = computed( () => { const pfpSource = computed( () => {
return profilePicture.value ?? "/src/assets/images/PFP_BearHead.svg" return user.value?.profilePictureFilename ?? "/src/assets/images/PFP_BearHead.svg"
}) })
function loginUser( user: User ) { function loginUser( userParam: User ) {
username.value = user.username; user.value = userParam;
profilePicture.value = user.profilePictureFilename ?? null;
loggedIn.value = true;
} }
function logoutUser() { function logoutUser() {
username.value = ''; user.value = null;
profilePicture.value = null;
loggedIn.value = false;
} }
function getCheckUser(): Promise<User> { function getCheckUser(): Promise<User> {
@ -43,8 +40,9 @@ export const useUserStore = defineStore( 'user', () => {
return { return {
//Refs //Refs
username, // username,
profilePicture, // profilePicture,
user,
loggedIn, loggedIn,
userCheckPromise, userCheckPromise,

View File

@ -18,5 +18,5 @@ export default defineConfig({
build:{ build:{
outDir: '../resources/static', outDir: '../resources/static',
emptyOutDir: true, emptyOutDir: true,
} },
}); });