updated npm versions; Refactored Question/Answer Types

This commit is contained in:
Baer 2026-03-21 22:56:02 +01:00
parent ae18ac1ccd
commit afd2345cac
47 changed files with 3564 additions and 3785 deletions

View File

@ -1,11 +1,10 @@
package at.eisibaer.jbear2.dto.board package at.eisibaer.jbear2.dto.board
import at.eisibaer.jbear2.dto.edit.AnswerTypeDto
import org.springframework.data.geo.Point import org.springframework.data.geo.Point
data class AnswerDto( data class AnswerDto(
val text: String, val text: String,
val answerType: AnswerTypeDto, val answerType: Int,
val image: String?, val image: String?,
val location: Point?, val location: Point?,
) )

View File

@ -1,11 +1,10 @@
package at.eisibaer.jbear2.dto.board package at.eisibaer.jbear2.dto.board
import at.eisibaer.jbear2.dto.edit.QuestionTypeDto
import org.springframework.data.geo.Point import org.springframework.data.geo.Point
data class QuestionDto( data class QuestionDto(
val text: String, val text: String,
val questionType: QuestionTypeDto, val questionType: Int,
val fontScaling: Int, val fontScaling: Int,
val image: String?, val image: String?,
val location: Point?, val location: Point?,

View File

@ -1,7 +0,0 @@
package at.eisibaer.jbear2.dto.edit
data class AnswerTypeDto(
val title: String,
val description: String,
val id: Long?,
)

View File

@ -1,7 +0,0 @@
package at.eisibaer.jbear2.dto.edit
data class QuestionTypeDto(
val title: String,
val description: String,
val id: Long,
)

View File

@ -1,6 +0,0 @@
package at.eisibaer.jbear2.dto.edit
data class TypesDto(
val questionTypes: List<QuestionTypeDto>,
val answerTypes: List<AnswerTypeDto>,
)

View File

@ -1,32 +0,0 @@
package at.eisibaer.jbear2.endpoint
import at.eisibaer.jbear2.dto.edit.TypesDto
import at.eisibaer.jbear2.repository.AnswerTypeRepository
import at.eisibaer.jbear2.repository.QuestionTypeRepository
import at.eisibaer.jbear2.service.mapper.AnswerTypeMapper
import at.eisibaer.jbear2.service.mapper.QuestionTypeMapper
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
@Controller
@RequestMapping("/api/board")
class BoardEndpoint(
val answerTypeRepository: AnswerTypeRepository,
val questionTypeRepository: QuestionTypeRepository,
val questionTypeMapper: QuestionTypeMapper,
val answerTypeMapper: AnswerTypeMapper,
) {
@GetMapping("/structure")
fun getStructureForBoardEditing(): ResponseEntity<TypesDto>{
val questionTypes = questionTypeRepository.findAllByActiveTrue()
val answerTypes = answerTypeRepository.findAllByActiveTrue()
val types = TypesDto(
questionTypeMapper.toDto( questionTypes ),
answerTypeMapper.toDto( answerTypes )
);
return ResponseEntity.ok(types);
}
}

View File

@ -1,5 +1,6 @@
package at.eisibaer.jbear2.model package at.eisibaer.jbear2.model
import at.eisibaer.jbear2.model.enums.AnswerType
import jakarta.persistence.* import jakarta.persistence.*
@Entity @Entity
@ -11,9 +12,9 @@ data class Answer(
@Column(name = "text", nullable = false, unique = false) @Column(name = "text", nullable = false, unique = false)
val text: String, val text: String,
@ManyToOne @Enumerated
@JoinColumn(name = "fk_answer_type", referencedColumnName = "id") @Column(name = "answer_type", nullable = false, unique = false)
val answerType: AnswerType, val answerType: AnswerType = AnswerType.TEXT,
@OneToOne @OneToOne
@JoinColumn(name = "fk_image_file", referencedColumnName = "id") @JoinColumn(name = "fk_image_file", referencedColumnName = "id")

View File

@ -1,21 +0,0 @@
package at.eisibaer.jbear2.model
import jakarta.persistence.*
@Entity
@Table(name = "answer_types")
data class AnswerType(
@Column(name = "title", nullable = false, unique = true)
val title: String,
@Column(name = "description", nullable = false, unique = true)
val description: String,
@Column(name = "active", nullable = false, unique = false)
val active: Boolean,
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
val id: Long? = null,
)

View File

@ -1,5 +1,6 @@
package at.eisibaer.jbear2.model package at.eisibaer.jbear2.model
import at.eisibaer.jbear2.model.enums.QuestionType
import jakarta.persistence.* import jakarta.persistence.*
@Entity @Entity
@ -13,9 +14,9 @@ data class Question(
@Column(name = "text", nullable = false, unique = false) @Column(name = "text", nullable = false, unique = false)
val text: String, val text: String,
@ManyToOne @Enumerated
@JoinColumn(name = "fk_question_type", referencedColumnName = "id") @Column(name = "question_type", nullable = false, unique = false)
val questionType: QuestionType, val questionType: QuestionType = QuestionType.TEXT,
@Column(name = "font_scaling", nullable = false, unique = false) @Column(name = "font_scaling", nullable = false, unique = false)
val fontScaling: Int, val fontScaling: Int,

View File

@ -1,21 +0,0 @@
package at.eisibaer.jbear2.model
import jakarta.persistence.*
@Entity
@Table(name = "question_types")
data class QuestionType(
@Column(name = "title", nullable = false, unique = true)
val title: String,
@Column(name = "description", nullable = false, unique = true)
val description: String,
@Column(name = "active", nullable = false, unique = false)
val active: Boolean,
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
val id: Long? = null,
)

View File

@ -0,0 +1,8 @@
package at.eisibaer.jbear2.model.enums
enum class AnswerType {
TEXT,
IMAGE,
AUDIO,
LOCATION;
}

View File

@ -0,0 +1,8 @@
package at.eisibaer.jbear2.model.enums
enum class QuestionType {
TEXT,
IMAGE,
AUDIO,
LOCATION;
}

View File

@ -1,11 +0,0 @@
package at.eisibaer.jbear2.repository
import at.eisibaer.jbear2.model.AnswerType
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface AnswerTypeRepository : JpaRepository<AnswerType, Long> {
fun findAllByActiveTrue(): List<AnswerType>;
}

View File

@ -1,11 +0,0 @@
package at.eisibaer.jbear2.repository
import at.eisibaer.jbear2.model.QuestionType
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface QuestionTypeRepository : JpaRepository<QuestionType, Long> {
fun findAllByActiveTrue(): List<QuestionType>;
}

View File

@ -3,6 +3,7 @@ package at.eisibaer.jbear2.service.mapper
import at.eisibaer.jbear2.dto.board.AnswerDto import at.eisibaer.jbear2.dto.board.AnswerDto
import at.eisibaer.jbear2.model.Answer import at.eisibaer.jbear2.model.Answer
import at.eisibaer.jbear2.model.File import at.eisibaer.jbear2.model.File
import at.eisibaer.jbear2.model.enums.AnswerType
import at.eisibaer.jbear2.repository.FileRepository import at.eisibaer.jbear2.repository.FileRepository
import org.mapstruct.Context import org.mapstruct.Context
import org.mapstruct.Mapper import org.mapstruct.Mapper
@ -31,4 +32,12 @@ abstract class AnswerMapper {
} }
} }
} }
fun map(type: AnswerType): Int {
return type.ordinal;
}
fun map(value: Int): AnswerType {
return AnswerType.entries[value];
}
} }

View File

@ -1,9 +0,0 @@
package at.eisibaer.jbear2.service.mapper
import at.eisibaer.jbear2.dto.edit.AnswerTypeDto
import at.eisibaer.jbear2.model.AnswerType
import org.mapstruct.Mapper
@Mapper
interface AnswerTypeMapper : EntityMapper<AnswerTypeDto, AnswerType> {
}

View File

@ -3,6 +3,7 @@ package at.eisibaer.jbear2.service.mapper
import at.eisibaer.jbear2.dto.board.QuestionDto import at.eisibaer.jbear2.dto.board.QuestionDto
import at.eisibaer.jbear2.model.File import at.eisibaer.jbear2.model.File
import at.eisibaer.jbear2.model.Question import at.eisibaer.jbear2.model.Question
import at.eisibaer.jbear2.model.enums.QuestionType
import at.eisibaer.jbear2.repository.FileRepository import at.eisibaer.jbear2.repository.FileRepository
import org.mapstruct.Context import org.mapstruct.Context
import org.mapstruct.Mapper import org.mapstruct.Mapper
@ -31,4 +32,12 @@ abstract class QuestionMapper {
} }
} }
} }
fun map(type: QuestionType): Int {
return type.ordinal;
}
fun map(value: Int): QuestionType {
return QuestionType.entries[value];
}
} }

View File

@ -1,9 +0,0 @@
package at.eisibaer.jbear2.service.mapper
import at.eisibaer.jbear2.dto.edit.QuestionTypeDto
import at.eisibaer.jbear2.model.QuestionType
import org.mapstruct.Mapper
@Mapper
interface QuestionTypeMapper : EntityMapper<QuestionTypeDto, QuestionType> {
}

View File

@ -1,14 +0,0 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript'
],
parserOptions: {
ecmaVersion: 'latest'
}
}

View File

@ -0,0 +1,24 @@
import { defineConfig } from "eslint/config";
import { js } from "@eslint/js";
import { FlagCompat } from "eslint/eslintrc";
import pluginVue from "eslint-plugin-vue";
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default defineConfig([{
...pluginVue.configs['flat/essential'],
extends: compat.extends(
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-typescript",
),
languageOptions: {
ecmaVersion: "latest",
parserOptions: {},
},
}]);

File diff suppressed because it is too large Load Diff

View File

@ -13,41 +13,37 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/fontawesome-svg-core": "~7.2.0",
"@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "~7.2.0",
"@fortawesome/vue-fontawesome": "^3.0.8", "@fortawesome/vue-fontawesome": "~3.1.3",
"@popperjs/core": "^2.11.8", "@stomp/stompjs": "~7.3.0",
"@stomp/stompjs": "^7.0.0", "@vuelidate/core": "~2.0.3",
"@vuelidate/core": "^2.0.3", "@vuelidate/validators": "~2.0.4",
"@vuelidate/validators": "^2.0.4", "@vueuse/core": "~14.2.1",
"@vueuse/core": "^11.1.0", "axios": "~1.13.6",
"axios": "^1.7.2", "bootstrap": "~5.3.8",
"bootstrap": "^5.3.3", "pinia": "~3.0.4",
"pinia": "^2.1.7", "vue": "~3.5.30",
"vue": "^3.4.21", "vue-draggable-plus": "~0.6.1",
"vue-draggable-plus": "^0.5.3", "vue-i18n": "~11.3.0",
"vue-i18n": "^9.13.1", "vue-router": "~5.0.3"
"vue-router": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.8.0", "@eslint/js": "~10.0.1",
"@tsconfig/node20": "^20.1.4", "@intlify/eslint-plugin-vue-i18n": "~4.3.0",
"@types/bootstrap": "^5.2.10", "@tsconfig/node24": "~24.0.4",
"@types/jsdom": "^21.1.6", "@types/bootstrap": "~5.2.10",
"@types/node": "^20.12.5", "@types/node": "~25.5.0",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "~6.0.5",
"@vue/eslint-config-typescript": "^13.0.0", "@vue/eslint-config-typescript": "~14.7.0",
"@vue/test-utils": "^2.4.5", "@vue/test-utils": "~2.4.6",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "~0.9.0",
"eslint": "^8.57.0", "eslint": "~10.0.3",
"eslint-plugin-vue": "^9.23.0", "eslint-plugin-vue": "~10.8.0",
"jsdom": "^24.0.0", "sass": "~1.98.0",
"npm-run-all2": "^6.1.2", "typescript": "~5.9.3",
"sass": "^1.77.6", "vite": "~8.0.0",
"typescript": "~5.4.0", "vite-plugin-vue-devtools": "~8.1.0",
"vite": "^5.2.8", "vitest": "~4.1.0"
"vite-plugin-vue-devtools": "^7.0.25",
"vitest": "^1.4.0",
"vue-tsc": "^2.0.11"
} }
} }

View File

@ -55,6 +55,8 @@ $breadcrumb-divider: quote(">");
$form-file-button-bg: var(--#{$prefix}secondary-bg); $form-file-button-bg: var(--#{$prefix}secondary-bg);
$form-file-button-hover-bg: var(--#{$prefix}tertiary-bg); $form-file-button-hover-bg: var(--#{$prefix}tertiary-bg);
// $btn-border-radius: 0;
// $card-border-radius: 0;
/* Bootstrap Color Map adjustments */ /* Bootstrap Color Map adjustments */
$custom-colors: ( $custom-colors: (

View File

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import type { Board } from '@/models/board/Board'; import type { Board } from '@/models/board/Board';
import { userService } from '@/services/UserService'; import { userService } from '@/services/UserService';
import IconJeobeardy from '../icons/IconJeobeardy.vue'; import IconJeobeardy from '../icons/IconJeobeardy.vue';
@ -78,7 +77,7 @@ userService
</template> </template>
<div v-if="props.withNewOption" class="col-4 mb-3"> <div v-if="props.withNewOption" class="col-4 mb-3">
<button <button
class="btn btn-outline-primary w-100 h-100 d-flex flex-column justify-content-center p-3" class="btn btn-outline-primary w-100 h-100 d-flex flex-column justify-content-center align-items-center gap-2"
@click="createNewBoard" @click="createNewBoard"
> >
Create new Board Create new Board

View File

@ -37,8 +37,12 @@ function boardEntrySelected(cIndex: number, bEIndex: number){
<template v-for="(category, categoryIndex) in props.board.categories " :key="category.name"> <template v-for="(category, categoryIndex) in props.board.categories " :key="category.name">
<div class="col pb-2"> <div class="col pb-2">
<div class="d-flex flex-column h-100"> <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"> <button
<!-- TODO make color changeable?? --> class="flex-fill board-card-max-height card bg-primary w-100 my-1"
@click="categorySelected(categoryIndex)"
:title="board.categories[categoryIndex].description"
:style="[board.categories[categoryIndex].customColor ? {'background-color': `${board.categories[categoryIndex].color} !important`} : {}]"
>
<div class="card-body d-flex align-items-center justify-content-center"> <div class="card-body d-flex align-items-center justify-content-center">
{{ category.name }} {{ category.name }}
</div> </div>

View File

@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { VueDraggable } from 'vue-draggable-plus'; import { VueDraggable } from 'vue-draggable-plus';
import type { Board } from '@/models/board/Board'; import type { Board } from '@/models/board/Board';
import { Question } from '@/models/board/Question'; import { Question } from '@/models/board/Question';
import { QuestionType } from '@/models/board/QuestionType'; import { QuestionType, type QuestionTypeType } from '@/models/board/QuestionType';
import { answerTypesKey, questionTypesKey } from '@/services/UtilService'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { AnswerType } from '@/models/board/AnswerType';
const { t } = useI18n(); const { t } = useI18n();
@ -16,8 +16,6 @@ const props = defineProps<{
categoryIndex: number, categoryIndex: number,
boardEntryIndex: number, boardEntryIndex: number,
}>(); }>();
const questionTypes = inject(questionTypesKey);
const answerTypes = inject(answerTypesKey);
const emit = defineEmits<{ const emit = defineEmits<{
editBoard: [], editBoard: [],
@ -30,9 +28,9 @@ const boardEntry = computed( () => {
}); });
const newQuestionText = ref( '' ); const newQuestionText = ref( '' );
const newQuestionType = ref<QuestionType | null>( (questionTypes?.value ?? [null])[0] ); const newQuestionType = ref<QuestionTypeType>(QuestionType.TEXT);
function addQuestion() { function addQuestion() {
if( boardEntry.value.questions.length >= 10 || newQuestionType.value === null) { if( boardEntry.value.questions.length >= 10) {
return; return;
} }
const newQuestion = new Question(newQuestionText.value, newQuestionType.value); const newQuestion = new Question(newQuestionText.value, newQuestionType.value);
@ -133,7 +131,7 @@ function openQuestion(categoryIndex: number, boardEntryIndex: number, questionIn
</div> </div>
<div class="text-truncate"> <div class="text-truncate">
<span class="ms-2"> <span class="ms-2">
{{ t(`board.question.types.title.${question.questionType.title}`) }} <FontAwesomeIcon v-if="question.questionType === QuestionType.TEXT" :icon="['fas', 'align-center']" />
<span class="fw-light"> <span class="fw-light">
({{ question.text.length === 0 ? 'No Text yet' : question.text }}) ({{ question.text.length === 0 ? 'No Text yet' : question.text }})
</span> </span>
@ -170,8 +168,8 @@ function openQuestion(categoryIndex: number, boardEntryIndex: number, questionIn
<div class="row mb-2"> <div class="row mb-2">
<div class="col"> <div class="col">
<select id="type-for-new-question" v-model="newQuestionType" class="form-select"> <select id="type-for-new-question" v-model="newQuestionType" class="form-select">
<template v-for="questionType in questionTypes" :key="questionType.id"> <template v-for="questionType in QuestionType" :key="questionType">
<option :value="questionType" :title="t(`board.question.types.description.${questionType.description}`)">{{ t(`board.question.types.title.${questionType.title}`) }}</option> <option :value="questionType" :title="t(`board.question.types.description.${questionType}`)">{{ t(`board.question.types.title.${questionType}`) }}</option>
</template> </template>
</select> </select>
</div> </div>
@ -186,8 +184,8 @@ function openQuestion(categoryIndex: number, boardEntryIndex: number, questionIn
<h4>{{ t('board.answer.label', 2) }}</h4> <h4>{{ t('board.answer.label', 2) }}</h4>
<label class="mt-2" for="type-for-answer">{{ t( "board.answer.types.label" ) }}</label> <label class="mt-2" for="type-for-answer">{{ t( "board.answer.types.label" ) }}</label>
<select id="type-for-answer" v-model="boardEntry.answer.answerType" class="form-select"> <select id="type-for-answer" v-model="boardEntry.answer.answerType" class="form-select">
<template v-for="answerType in answerTypes" :key="answerType.id"> <template v-for="answerType in AnswerType" :key="answerType">
<option :value="answerType" :title="t(`board.answer.types.description.${answerType.description}`)">{{ t(`board.answer.types.title.${answerType.title}`) }}</option> <option :value="answerType" :title="t(`board.answer.types.description.${answerType}`)">{{ t(`board.answer.types.title.${answerType}`) }}</option>
</template> </template>
</select> </select>
<label class="mt-2" for="answer-text">{{ t( 'board.answer.text' ) }}</label> <label class="mt-2" for="answer-text">{{ t( 'board.answer.text' ) }}</label>

View File

@ -4,7 +4,6 @@ import { Category } from '@/models/board/Category';
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { VueDraggable } from 'vue-draggable-plus'; import { VueDraggable } from 'vue-draggable-plus';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
const { t } = useI18n(); const { t } = useI18n();

View File

@ -1,12 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Board } from '@/models/board/Board'; import type { Board } from '@/models/board/Board';
import { inject, ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { VueDraggable } from 'vue-draggable-plus'; import { VueDraggable } from 'vue-draggable-plus';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { BoardEntry } from '@/models/board/BoardEntry'; import { BoardEntry } from '@/models/board/BoardEntry';
import { Answer } from '@/models/board/Answer'; import { Answer } from '@/models/board/Answer';
import { answerTypesKey } from '@/services/UtilService';
import { AnswerType } from '@/models/board/AnswerType'; import { AnswerType } from '@/models/board/AnswerType';
const { t } = useI18n(); const { t } = useI18n();
@ -16,7 +14,6 @@ const board = defineModel<Board>( { required: true } );
const props = defineProps<{ const props = defineProps<{
categoryIndex: number, categoryIndex: number,
}>(); }>();
const answerTypes = inject(answerTypesKey);
const emit = defineEmits<{ const emit = defineEmits<{
editBoardEntry: [categoryIndex: number, boardEntryIndex: number], editBoardEntry: [categoryIndex: number, boardEntryIndex: number],
@ -29,7 +26,7 @@ function addBoardEntry() {
if( board.value.categories[props.categoryIndex].boardEntries.length >= 10 ) { if( board.value.categories[props.categoryIndex].boardEntries.length >= 10 ) {
return; return;
} }
const answer = new Answer('', answerTypes?.value[0] ?? new AnswerType('', '', false, 0)); const answer = new Answer('', AnswerType.TEXT);
const newBoardEntry = new BoardEntry( newBoardEntryName.value, answer, [] ); const newBoardEntry = new BoardEntry( newBoardEntryName.value, answer, [] );
board.value.categories[props.categoryIndex].boardEntries.push( newBoardEntry ); board.value.categories[props.categoryIndex].boardEntries.push( newBoardEntry );
} }
@ -81,6 +78,11 @@ function openBoard() {
<label for="category-description">{{ t( 'board.category.description' ) }}</label> <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 id="category-description" class="form-control" v-model="board.categories[props.categoryIndex].description" :placeholder="t( 'board.category.description' )">
</textarea> </textarea>
<div class="form-check mt-2">
<input id="category-color-custom" type="checkbox" class="form-check-input" v-model="board.categories[props.categoryIndex].customColor">
<label for="category-color-custom">{{ t( 'board.category.custom-color' ) }}</label>
</div>
<input v-if="board.categories[props.categoryIndex].customColor" id="category-color" type="color" class="form-control" v-model="board.categories[props.categoryIndex].color"/>
</div> </div>
</div> </div>
<div class="row mt-3"> <div class="row mt-3">

View File

@ -1,12 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Board } from '@/models/board/Board'; import type { Board } from '@/models/board/Board';
import { computed, inject } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { QuestionType } from '@/models/board/QuestionType';
import { questionTypesKey } from '@/services/UtilService';
const QUESTION_TYPE_IMAGE_ID = 2;
const QUESTION_TYPE_AUDIO_ID = 3;
const { t } = useI18n(); const { t } = useI18n();
@ -17,7 +13,6 @@ const props = defineProps<{
boardEntryIndex: number, boardEntryIndex: number,
questionIndex: number; questionIndex: number;
}>(); }>();
const questionTypes = inject( questionTypesKey );
const emit = defineEmits<{ const emit = defineEmits<{
editBoard: [], editBoard: [],
@ -85,7 +80,7 @@ function newAudioUploaded( event: Event ) {
</a> </a>
</li> </li>
<li class="breadcrumb-item active text-truncate" style="max-width: 6em;" aria-current="page"> <li class="breadcrumb-item active text-truncate" style="max-width: 6em;" aria-current="page">
{{ question.text.length !== 0 ? question.text : question.questionType.title }} {{ question.text.length !== 0 ? question.text : t(`board.question.types.title.${question.questionType}`) }}
</li> </li>
</ol> </ol>
</nav> </nav>
@ -107,12 +102,12 @@ function newAudioUploaded( event: Event ) {
<label for="board-entry-points">{{ t( 'board.question.type' ) }}</label> <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"> <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"> <template v-for=" questionType in QuestionType " :key="questionType">
<option :value="questionType" :title="t(`board.question.types.description.${questionType.description}`)">{{ t(`board.question.types.title.${questionType.title}`) }}</option> <option :value="questionType" :title="t(`board.question.types.description.${questionType}`)">{{ t(`board.question.types.title.${questionType}`) }}</option>
</template> </template>
</select> </select>
<template v-if=" question.questionType.id === QUESTION_TYPE_IMAGE_ID "> <template v-if=" question.questionType === QuestionType.IMAGE ">
<label for="question-image-input">{{ t( 'board.question.upload.image' ) }}</label> <label for="question-image-input">{{ t( 'board.question.upload.image' ) }}</label>
<input <input
id="question-image-input" id="question-image-input"
@ -122,7 +117,7 @@ function newAudioUploaded( event: Event ) {
accept="image/png, image/jpeg" accept="image/png, image/jpeg"
> >
</template> </template>
<template v-if=" question.questionType.id === QUESTION_TYPE_AUDIO_ID "> <template v-if=" question.questionType === QuestionType.AUDIO ">
<label for="question-audio-input">{{ t( 'board.question.upload.audio' ) }}</label> <label for="question-audio-input">{{ t( 'board.question.upload.audio' ) }}</label>
<input <input
id="question-audio-input" id="question-audio-input"

View File

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';

View File

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { onMounted, watch } from 'vue'; import { onMounted, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';

View File

@ -8,7 +8,6 @@ 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",

View File

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useGameStore } from '@/stores/GameStore'; import { useGameStore } from '@/stores/GameStore';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { t } = useI18n(); const { t } = useI18n();
@ -10,8 +9,6 @@ const gameStore = useGameStore();
function adjustPoints(){ function adjustPoints(){
} }
</script> </script>
<template> <template>
@ -32,7 +29,7 @@ function adjustPoints(){
/> />
</div> </div>
</div> </div>
<template v-for="player in gameStore.players" :key="player.id"> <template v-for="player in gameStore.players" :key="player.uuid">
<div class="card bg-body-secondary mb-2"> <div class="card bg-body-secondary mb-2">
<div class="card-header bg-primary p-2"> <div class="card-header bg-primary p-2">
<div class="row"> <div class="row">

View File

@ -1,16 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject, provide, ref, type Ref } from 'vue'; import { computed, inject, ref, type Ref } from 'vue';
import { import { navbarKey } from '@/services/UtilService';
answerTypesKey,
navbarKey,
questionTypesKey,
} from '@/services/UtilService';
import { boardService } from '@/services/BoardService';
import { Board } from '@/models/board/Board'; import { Board } from '@/models/board/Board';
import { BoardEntry } from '@/models/board/BoardEntry'; import { BoardEntry } from '@/models/board/BoardEntry';
import { QuestionType } from '@/models/board/QuestionType';
import { AnswerType } from '@/models/board/AnswerType';
import type NavBar from '@/components/blocks/NavBar.vue'; import type NavBar from '@/components/blocks/NavBar.vue';
import BoardView from '@/components/blocks/BoardView.vue'; import BoardView from '@/components/blocks/BoardView.vue';
@ -18,7 +11,6 @@ import CreatePanel from '@/components/blocks/CreatePanel.vue';
import BoardEntryView from '@/components/blocks/BoardEntryView.vue'; import BoardEntryView from '@/components/blocks/BoardEntryView.vue';
import { userService } from '@/services/UserService'; import { userService } from '@/services/UserService';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
const navbar = inject<Ref<InstanceType<typeof NavBar> | null>>(navbarKey); const navbar = inject<Ref<InstanceType<typeof NavBar> | null>>(navbarKey);
const navbarHeight = computed(() => { const navbarHeight = computed(() => {
@ -106,15 +98,6 @@ function exit() {
//TODO //TODO
} }
let answerTypes = ref<Array<AnswerType>>([]);
let questionTypes = ref<Array<QuestionType>>([]);
provide(questionTypesKey, questionTypes);
provide(answerTypesKey, answerTypes);
boardService.getTypes().then((typesDto) => {
answerTypes.value = typesDto.answerTypes;
questionTypes.value = typesDto.questionTypes;
});
const boardLoading = ref(true); const boardLoading = ref(true);
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();

View File

@ -1,17 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject, provide, ref, type Ref } from 'vue'; import { computed, inject, ref, type Ref } from 'vue';
import { import { navbarKey } from '@/services/UtilService';
answerTypesKey,
navbarKey,
questionTypesKey,
} from '@/services/UtilService';
import { boardService } from '@/services/BoardService';
import { Board } from '@/models/board/Board'; import { Board } from '@/models/board/Board';
import { Category } from '@/models/board/Category'; import { Category } from '@/models/board/Category';
import { BoardEntry } from '@/models/board/BoardEntry'; import { BoardEntry } from '@/models/board/BoardEntry';
import { Answer } from '@/models/board/Answer'; import { Answer } from '@/models/board/Answer';
import { QuestionType } from '@/models/board/QuestionType';
import { AnswerType } from '@/models/board/AnswerType'; import { AnswerType } from '@/models/board/AnswerType';
import type NavBar from '@/components/blocks/NavBar.vue'; import type NavBar from '@/components/blocks/NavBar.vue';
@ -41,7 +35,7 @@ const board1 = ref<Board>(
); );
const answer = new Answer( const answer = new Answer(
'', '',
new AnswerType('Simple Answer', 'Simple Answer with text', true, 1) AnswerType.TEXT
); );
const newBoardEntry = new BoardEntry( const newBoardEntry = new BoardEntry(
'Test Entry 1', 'Test Entry 1',
@ -89,15 +83,6 @@ function showAnswer() {
function hideAnswer() { function hideAnswer() {
isAnswerShown.value = false; isAnswerShown.value = false;
} }
let answerTypes = ref<Array<AnswerType>>([]);
let questionTypes = ref<Array<QuestionType>>([]);
provide(questionTypesKey, questionTypes);
provide(answerTypesKey, answerTypes);
boardService.getTypes().then((typesDto) => {
answerTypes.value = typesDto.answerTypes;
questionTypes.value = typesDto.questionTypes;
});
</script> </script>
<template> <template>

View File

@ -8,7 +8,6 @@ import { useGameStore } from '@/stores/GameStore';
import IconJeobeardy from '@/components/icons/IconJeobeardy.vue'; import IconJeobeardy from '@/components/icons/IconJeobeardy.vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import type { AxiosError } from 'axios'; import type { AxiosError } from 'axios';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
const { t } = useI18n(); const { t } = useI18n();

View File

@ -2,7 +2,6 @@
import { GAME_STATUS_CONST } from '@/services/GameService'; import { GAME_STATUS_CONST } from '@/services/GameService';
import { useGameStore } from '@/stores/GameStore'; import { useGameStore } from '@/stores/GameStore';
import { useUserStore } from '@/stores/UserStore'; import { useUserStore } from '@/stores/UserStore';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import type { AxiosError } from 'axios'; import type { AxiosError } from 'axios';
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';

View File

@ -2,7 +2,6 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useClipboard } from '@vueuse/core'; import { useClipboard } from '@vueuse/core';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { userService } from '@/services/UserService'; import { userService } from '@/services/UserService';
import { useGameStore } from '@/stores/GameStore'; import { useGameStore } from '@/stores/GameStore';

View File

@ -79,6 +79,7 @@
"category": { "category": {
"infos": "Category Infos", "infos": "Category Infos",
"label": "No Category | Category | Categories | {count} Categories", "label": "No Category | Category | Categories | {count} Categories",
"custom-color": "Set Custom Color",
"name": "Category Name", "name": "Category Name",
"description": "Category Description", "description": "Category Description",
"add": "Add Category", "add": "Add Category",
@ -100,16 +101,16 @@
"fontsize": "Font Size", "fontsize": "Font Size",
"types": { "types": {
"title": { "title": {
"simple_question": "Simple Question", "0": "Simple Question",
"image_question": "Image Question", "1": "Image Question",
"audio_question": "Audio Question", "2": "Audio Question",
"location_question": "Location Question" "3": "Location Question"
}, },
"description": { "description": {
"simple_question_description": "A simple question with just text", "0": "A simple question with just text",
"image_question_description": "A question with text and an image", "1": "A question with text and an image",
"audio_question_description": "A question with text and audio", "2": "A question with text and audio",
"location_question_description": "A question with an image for players to guess the location on the image" "3": "A question with an image for players to guess the location on the image"
} }
}, },
"upload": { "upload": {
@ -123,14 +124,16 @@
"types": { "types": {
"label": "Answer Type", "label": "Answer Type",
"title": { "title": {
"simple_answer": "Simple Answer", "0": "Simple Answer",
"image_answer": "Image Answer", "1": "Image Answer",
"location_answer": "Location Answer" "2": "Audio Answer",
"3": "Location Answer"
}, },
"description": { "description": {
"simple_answer_description": "A simple answer with just text", "0": "A simple answer with just text",
"image_answer_description": "An answer with text and an image", "1": "An answer with text and an image",
"location_answer_description": "An answer for a location question with the correct location shown on an image" "2": "An answer with text and audio",
"3": "An answer for a location question with the correct location shown on an image"
} }
} }
} }
@ -168,7 +171,7 @@
"light": { "light": {
"name": "Light" "name": "Light"
}, },
"highContrast": { "high-contrast": {
"name": "High Contrast" "name": "High Contrast"
} }
}, },

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, faPlus, faTrash, faGripLines, faAngleUp, faAngleDown, faAngleLeft, faAnglesRight, faHandPointer, faMinus, faEye, faLock, faLockOpen, faSave, faXmark, faUsers, faArrowRightToBracket, faCopy, faCheck, faArrowRight } from '@fortawesome/free-solid-svg-icons'; import { faSun, faMoon, faCircleHalfStroke, faEdit, faPlay, faSpinner, faLanguage, faGlobe, faPlus, faTrash, faGripLines, faAngleUp, faAngleDown, faAngleLeft, faAnglesRight, faHandPointer, faMinus, faEye, faLock, faLockOpen, faSave, faXmark, faUsers, faArrowRightToBracket, faCopy, faCheck, faArrowRight, faAlignCenter } 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';
@ -52,10 +52,12 @@ library.add(
faCopy, faCopy,
faCheck, faCheck,
faArrowRight, faArrowRight,
faAlignCenter,
) )
const app = createApp( App ); const app = createApp( App );
app.component('FontAwesomeIcon', FontAwesomeIcon);
app.use( createPinia() ); app.use( createPinia() );
app.use( router ); app.use( router );
app.use( i18n ); app.use( i18n );

View File

@ -1,8 +1,8 @@
export class AnswerType { export const AnswerType = {
constructor( TEXT: 0,
public title: string, IMAGE: 1,
public description: string, AUDIO: 2,
public active: boolean, LOCATION: 3,
public id: number, } as const
){}
} export type AnswerTypeType = (typeof AnswerType)[keyof typeof AnswerType];

View File

@ -6,5 +6,7 @@ export class Category{
public description: string, public description: string,
public boardEntries: Array<BoardEntry> = [], public boardEntries: Array<BoardEntry> = [],
public id: number | undefined = undefined, public id: number | undefined = undefined,
public customColor: boolean = false,
public color: string = '#e86a92ff',
){} ){}
} }

View File

@ -1,9 +1,9 @@
import type { QuestionType } from './QuestionType'; import type { QuestionTypeType } from './QuestionType';
export class Question{ export class Question{
constructor( constructor(
public text: string, public text: string,
public questionType: QuestionType, public questionType: QuestionTypeType,
public fontScaling: number = 3, public fontScaling: number = 3,
public image: string | undefined = undefined, public image: string | undefined = undefined,
public audio: string | undefined = undefined, public audio: string | undefined = undefined,

View File

@ -1,8 +1,8 @@
export class QuestionType { export const QuestionType = {
constructor( TEXT: 0,
public title: string, IMAGE: 1,
public description: string, AUDIO: 2,
public active: boolean, LOCATION: 3,
public id: number | undefined = undefined, } as const
){}
} export type QuestionTypeType = (typeof QuestionType)[keyof typeof QuestionType];

View File

@ -5,6 +5,7 @@ import axios from "axios";
class BoardService { class BoardService {
//TODO Remove and replace with const values
getTypes(): Promise<TypesDto> { getTypes(): Promise<TypesDto> {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
axios.get( `${ENV.API_BASE_URL}/board/structure`, axios.get( `${ENV.API_BASE_URL}/board/structure`,

View File

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

View File

@ -1,5 +1,5 @@
{ {
"extends": "@tsconfig/node20/tsconfig.json", "extends": "@tsconfig/node24/tsconfig.json",
"include": [ "include": [
"vite.config.*", "vite.config.*",
"vitest.config.*", "vitest.config.*",