diff --git a/FullstackProsjekt/src/frontend/src/JSONparser.js b/FullstackProsjekt/src/frontend/src/JSONparser.js new file mode 100644 index 0000000000000000000000000000000000000000..039e5630076b14103dbbaff20ce21d9e2dcd02bc --- /dev/null +++ b/FullstackProsjekt/src/frontend/src/JSONparser.js @@ -0,0 +1,22 @@ +//methods for constructing Quiz and Question-objects. Maybe instead do this in api service? + +//JSON to quiz-object(s) +//JSON to question-object(s) + +//quiz-object to JSON +//question-object to JSON + +//quiz questions: gets questionIds, send this to questionCards? + +//Later: splitting into packages + +/** +QuizDTO: +Integer id; +String title; +List<Integer> questionIds; +Integer creatorId; +QuizCategory category; +QuizDifficulty difficulty; +*/ + diff --git a/FullstackProsjekt/src/frontend/src/components/shared/AnswerCard.vue b/FullstackProsjekt/src/frontend/src/components/shared/AnswerCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..0a261c2c8c3b52a4a3c7b59328c4de31b43717ac --- /dev/null +++ b/FullstackProsjekt/src/frontend/src/components/shared/AnswerCard.vue @@ -0,0 +1,66 @@ +<script> +import { RouterLink, RouterView } from 'vue-router'; +import router from "@/router/index.js"; +import { useRouter } from 'vue-router'; +export default { + props: { + answerId: { + type: Number, + required: true, + }, + answer: { + type: String, + required: true, + }, + correct: { + type: Boolean, + required: false + } + }, + methods: { + deleteAnswer() { + + } + } +} +</script> + +<template> + <div class="question-wrapper"> + + <h3>{{answer}}</h3> + <div class="quiz-footer"> + <button @click="deleteAnswer" class="delete-btn">Delete</button> + </div> + </div> +</template> + +<style> +.overViewQuestion-page{ + padding: 50px; +} + +.quiz-header{ + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} +.quiz-body{ + margin-bottom: 40px; +} + +.question-wrapper { + display: flex; + width: 80%; + justify-content: space-between; +} + +.quiz-footer{ + display: flex; + justify-content: space-evenly; + align-items: center; + padding-left: 25px; + padding-right: 25px; +} +</style> \ No newline at end of file diff --git a/FullstackProsjekt/src/frontend/src/components/shared/EditQuestionModel.vue b/FullstackProsjekt/src/frontend/src/components/shared/EditQuestionModel.vue new file mode 100644 index 0000000000000000000000000000000000000000..858f0e0df6378637819feea987ef6fc479cc0c1a --- /dev/null +++ b/FullstackProsjekt/src/frontend/src/components/shared/EditQuestionModel.vue @@ -0,0 +1,161 @@ +<script> +/* +<script setup> +const props = defineProps({ + show: Boolean +})*/ + +export default { + props: { + questionId: { + type: Number, + required: true + } + }, + mounted() { + //APi req + }, + data() { + return { + questionText: '', + correctIndex: 0, + answers: [ + {answerId: 0, answer: 'first answer', correct: true}, + {answerId: 1, answer: 'second answer', correct: false}, + {answerId: 2, answer: 'third answer', correct: false} + ] + } + }, + + methods: { + closeModal() { + this.$emit('close'); + }, + newAnswer() { + //default: not correct! + }, + } + +}; +</script> + +<template> + <div class="modal-overlay" @click="closeModal"> + <div @click.stop class="modal-mask"> + <div class="modal-container"> + <div class="question-title"> + <h3>Question: </h3> + <input v-model="questionText" placeholder="Type your question here"> + </div> + <div class="modal-body"> + <table class="table"> + <thead> + <tr> + <th scope="col">#</th> + <th scope="col">Answer</th> + <th scope="col">Correct ?</th> + </tr> + </thead> + <tbody> + <tr v-for="answer in answers"> + <th scope="row">{{answer.answer}}</th> + <td> + <input type="text" v-model="answer.answer" id="questionInput"> + </td> + <td> + <input :checked="answer.correct_answer === 1" class="form-check-input" :value="answer.id" @change="handleRadioToggle(answer.id)" type="radio"> + </td> + </tr> + </tbody> + </table> + </div> + <div class="modal-footer"> + default footer + <button + class="modal-default-button" + @click="$emit('close')"> + OK + </button> + </div> + </div> + </div> + </div> + <!-- + + <Transition name="modal"> + <div v-if="show" class="modal-mask"> + <div class="modal-container"> + <div class="modal-header"> + <slot name="header">default header</slot> + </div> + + <div class="modal-body"> + <slot name="body">default body</slot> + </div> + + <div class="modal-footer"> + <slot name="footer"> + default footer + <button + class="modal-default-button" + @click="$emit('close')" + >OK</button> + </slot> + </div> + </div> + </div> + </Transition> + --> +</template> + +<style> +.modal-mask { + position: fixed; + z-index: 9998; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + transition: opacity 0.3s ease; +} + +.modal-container { + width: 500px; + margin: auto; + padding: 20px 30px; + background-color: #fff; + border-radius: 2px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33); + transition: all 0.3s ease; +} + +.question-title { + display: flex; + justify-content: space-between; +} + + + + +.modal-header h5 { + margin-top: 0; + color: #363636; +} + +.modal-body { + margin: 20px 0; +} + +.modal-default-button { + float: right; +} + + +.modal-enter-from .modal-container, +.modal-leave-to .modal-container { + -webkit-transform: scale(1.1); + transform: scale(1.1); +} +</style> \ No newline at end of file diff --git a/FullstackProsjekt/src/frontend/src/components/shared/NewQuestionModel.vue b/FullstackProsjekt/src/frontend/src/components/shared/NewQuestionModel.vue index f04f8fdf7ea5f415b19a6f132c1bdfe16af18243..b502ac6b73757b77db8798c8b99c021dd75cb282 100644 --- a/FullstackProsjekt/src/frontend/src/components/shared/NewQuestionModel.vue +++ b/FullstackProsjekt/src/frontend/src/components/shared/NewQuestionModel.vue @@ -1,10 +1,103 @@ -<script setup> -const props = defineProps({ - show: Boolean -}) +<script> +import {apiClient} from "@/api.js"; +export default { + props: { + quizId: { + type: Number, + required: true + } + }, + data() { + return { + questionText: '', + answers: [{ text: '', correct: false }], + correctAnswerIndex: 0, + type: 'MC', + score: 0, + correctAnswer: null, + errorMsg: '' + } + }, + methods: { + //TODO: error prevention/handling + async handleSubmit() { + try { + this.findCorrectAnswer(); + await apiClient.post('/questions', { + quizId: this.quizId, + questionText: this.questionText, + type: this.type, + answer: this.correctAnswer, + options: this.answers.map(answer => answer.text), //just the strings! + score: this.score + //add questionId to questions in editQuiz! + + }) + } catch (error) { + this.errorMsg = 'Error signing up'; + } + }, + closeModal() { + this.$emit('close'); + }, + newAnswer() { + this.answers.push({ text: '', correct: false }); + }, + findCorrectAnswer(){ + if (this.correctAnswerIndex !== null && this.answers[this.correctAnswerIndex]) { + this.correctAnswer = this.answers[this.correctAnswerIndex].text; + } + } + } + +}; </script> <template> + <div class="modal-overlay" @click="closeModal"> + <div @click.stop class="modal-mask"> + <div class="modal-container"> + <form @submit.prevent="handleSubmit"> + <div class="question-title"> + <h3>Question:</h3> + <input v-model="questionText" placeholder="Type your question here"> + + <label>Score:</label> + <input type="number" id="scoreInput" v-model="score"> + </div> + <div class="modal-body"> + <!-- + <AnswerCard answer-id="answerCard" v-for="answer in answers" + :key="answer.id" :answerId="answer.id" :answer="answer.answer" :correct="answer.correct"/> + --> + <table class="table"> + <thead> + <tr> + <th scope="col">Answer</th> + <th scope="col">Correct ?</th> + </tr> + </thead> + <tbody> + <tr v-for="(answer, index) in answers"> + <td><input type="text" v-model="answer.text"></td> + <td> + <input type="radio" :id="'correctAnswer_' + index" :value="index" v-model="correctAnswerIndex"> + <label :for="'correctAnswer_' + index">Correct</label> + </td> + </tr> + </tbody> + </table> + </div> + <div class="modal-footer"> + <button class="edit-btn" @click="newAnswer">Add answer</button> + <button class="modal-default-button" @click="$emit('close')">SUBMIT</button> + </div> + </form> + </div> + </div> + </div> + <!-- + <Transition name="modal"> <div v-if="show" class="modal-mask"> <div class="modal-container"> @@ -28,6 +121,7 @@ const props = defineProps({ </div> </div> </Transition> + --> </template> <style> @@ -53,6 +147,14 @@ const props = defineProps({ transition: all 0.3s ease; } +.question-title { + display: flex; + justify-content: space-between; +} + + + + .modal-header h5 { margin-top: 0; color: #363636; diff --git a/FullstackProsjekt/src/frontend/src/components/shared/QuestionCard.vue b/FullstackProsjekt/src/frontend/src/components/shared/QuestionCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..c5fe7d7972238ff17d2b6ef5240c3e19c42cef05 --- /dev/null +++ b/FullstackProsjekt/src/frontend/src/components/shared/QuestionCard.vue @@ -0,0 +1,80 @@ +<script> +import { RouterLink, RouterView } from 'vue-router'; +import router from "@/router/index.js"; +import { useRouter } from 'vue-router'; +import EditQuestionModel from "@/components/shared/EditQuestionModel.vue"; +export default { + components: {EditQuestionModel}, + props: { + questionId: { + type: Number, + required: true, + } + }, + mounted() { + //API req, get question from id + }, + data() { + return { + showEditQuestion: false, + questionNum: 0, + question: 'question text' + } + }, + methods: { + deleteQuestion() { + //API req, delete question + }, + editQuestion() { + this.showEditQuestion = true; + console.log(this.showEditQuestion); + }, + hideEditQuestion() { + this.showEditQuestion = false; + //TODO: update answers, +answer count + } + } +} +</script> + +<template> + <div class="question-wrapper"> + <h4>{{questionId}}</h4> + <h3>{{question}}</h3> + <div class="quiz-footer"> + <button @click="editQuestion" class="edit-btn">Edit</button> + <button @click="deleteQuestion" class="delete-btn">Delete</button> + </div> + <EditQuestionModel :question-id=questionId v-if="this.showEditQuestion" @close="hideEditQuestion"/> + </div> +</template> + +<style> +.overViewQuestion-page{ + padding: 50px; +} + +.quiz-header{ + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} +.quiz-body{ + margin-bottom: 40px; +} + +.question-wrapper { + display: flex; + width: 80%; + justify-content: space-between; +} + +.quiz-footer{ + display: flex; + justify-content: space-evenly; + align-items: center; + padding-left: 25px; + padding-right: 25px; +} +</style> \ No newline at end of file diff --git a/FullstackProsjekt/src/frontend/src/components/shared/QuizCard.vue b/FullstackProsjekt/src/frontend/src/components/shared/QuizCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..a56765a8b59ef4d2ec94ebb3efebabbb345b259e --- /dev/null +++ b/FullstackProsjekt/src/frontend/src/components/shared/QuizCard.vue @@ -0,0 +1,92 @@ +<script> +import router from "@/router/index.js"; +import {apiClient} from "@/api.js"; + +export default { + props: { + quizId: { + type: Number, + required: true, + } + }, + data() { + return { + quizTitle: null, + quizDifficulty: null, + quizCategory: null + } + + }, + mounted() { + this.getQuiz(); + }, + methods: { + async getQuiz() { + console.log('Fetching data for quiz: ', this.quizId); + + try { + await apiClient.get('/quiz/quiz/' + this.quizId).then(response => { + this.quizTitle = JSON.parse(response.data.title); + this.category = JSON.parse(response.data.category); + this.difficulty = JSON.parse(response.data.difficulty); + }); + } catch (error) { + //TODO: proper error handling + this.errorMsg = 'Error retrieving quizzes'; + } + }, + //link to pages, play quiz, edit, delete, with quizId + playQuiz() { + //create new router-method to playQuiz, using quizId + router.push({name: 'playQuiz', params: {quizId: this.quizId}}); + }, + editQuiz() { + //create new router-method to editQuiz, using quizId + router.push({name: 'editQuiz', params: {quizId: this.quizId}}); + }, + } +} +</script> + +<template> + <div class="course-col"> + <div class="quiz-header"> + <h3>{{ quizTitle }}</h3> + <!-- + <Svg :name="selectedIcon" /> + --> + </div> + <div class="quiz-body"> + <p>{{ quizDifficulty }}</p> + <p>{{ quizCategory }}</p> + </div> + <div class="quiz-footer"> + <button @click="playQuiz" class="play-btn">Play</button> + <button @click="editQuiz" class="edit-btn">Edit</button> + </div> + </div> +</template> + +<style> +.overViewQuestion-page{ + padding: 50px; +} + +.quiz-header{ + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} +.quiz-body{ + margin-bottom: 40px; +} + +.quiz-footer{ + display: flex; + justify-content: space-evenly; + align-items: center; + padding-left: 25px; + padding-right: 25px; +} +</style> \ No newline at end of file diff --git a/FullstackProsjekt/src/frontend/src/data/categories.js b/FullstackProsjekt/src/frontend/src/data/categories.js new file mode 100644 index 0000000000000000000000000000000000000000..8e1a6835c7bea6e1cd7e0cf3f0ad7e44933877cb --- /dev/null +++ b/FullstackProsjekt/src/frontend/src/data/categories.js @@ -0,0 +1,19 @@ +export const categoryEnums = [ + 'BUSINESS', + 'ART', + 'COMPUTER_SCIENCE', + 'CULTURE_AND_TRADITIONS', + 'FINANCE', + 'GENERAL_KNOWLEDGE', + 'GEOGRAPHY', + 'HISTORY', + 'LANGUAGES', + 'LAW', + 'MATH', + 'MUSIC', + 'SCIENCE', + 'SEASONAL', + 'SOCIAL_EMOTIONAL_LEARNING', + 'SOCIAL_STUDIES', + 'TRIVIA' +]; \ No newline at end of file diff --git a/FullstackProsjekt/src/frontend/src/data/difficulties.js b/FullstackProsjekt/src/frontend/src/data/difficulties.js new file mode 100644 index 0000000000000000000000000000000000000000..46d943b58df83180890d6e40d74eb4cd9d7db08f --- /dev/null +++ b/FullstackProsjekt/src/frontend/src/data/difficulties.js @@ -0,0 +1,5 @@ +export const difficultyEnums = [ + 'EASY', + 'MEDIUM', + 'HARD' +] \ No newline at end of file diff --git a/FullstackProsjekt/src/frontend/src/models/question.js b/FullstackProsjekt/src/frontend/src/models/question.js new file mode 100644 index 0000000000000000000000000000000000000000..52082dd3e95489adfb7957492e516335d23e5a9e --- /dev/null +++ b/FullstackProsjekt/src/frontend/src/models/question.js @@ -0,0 +1,11 @@ +export default class Question { + constructor(questionId, quizId, score, questionText, answers) { + this.questionId = questionId; + this.quizId = quizId; + this.score = score; + this.questionText = questionText; + this.answers = answers; + } +} + +//method for parsing JSON to question-objects, and opposite? Perhaps better to delegate to a JSON parsing class \ No newline at end of file diff --git a/FullstackProsjekt/src/frontend/src/models/quiz.js b/FullstackProsjekt/src/frontend/src/models/quiz.js new file mode 100644 index 0000000000000000000000000000000000000000..8099a99587f29816a631ee5be2a5c52586cb9ce7 --- /dev/null +++ b/FullstackProsjekt/src/frontend/src/models/quiz.js @@ -0,0 +1,24 @@ +export default class Quiz { + constructor(quizId, title, creatorId, questions, category, difficulty) { + this.quizId = quizId; + this.title = title; + this.creatorId = creatorId; + this.questions = questions; + this.category = category; + this.difficulty = difficulty; + } + + + + +} + +/** + QuizDTO: + Integer id; + String title; + List<Integer> questionIds; + Integer creatorId; + QuizCategory category; + QuizDifficulty difficulty; + */ diff --git a/FullstackProsjekt/src/frontend/src/router/index.js b/FullstackProsjekt/src/frontend/src/router/index.js index 41c86ae42b3abf3c25d4fefca20894319c4dc97e..80e8d9264e8eb9d8ada713d1c92e830d4e1fdf76 100644 --- a/FullstackProsjekt/src/frontend/src/router/index.js +++ b/FullstackProsjekt/src/frontend/src/router/index.js @@ -1,5 +1,6 @@ import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' +import EditQuizView from "@/views/EditQuizView.vue"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -37,7 +38,7 @@ const router = createRouter({ { path: '/createQuiz', name: 'create Quiz', - component: () => import('../views/EditQuizView.vue') + component: () => import('../views/NewQuizView.vue') }, { path: '/overviewQuiz', @@ -45,9 +46,16 @@ const router = createRouter({ component: () => import('../views/OverviewQuizView.vue') }, { - path: '/play-quiz', + path: '/play-quiz/:quizId', name: 'playQuiz', - component: () => import('../views/PlayQuizView.vue') + component: () => import('../views/PlayQuizView.vue'), + params: true + }, + { + path: '/edit-quiz/:quizId', + name: 'editQuiz', + component: EditQuizView, + params: true }, ] }) diff --git a/FullstackProsjekt/src/frontend/src/tokenController.js b/FullstackProsjekt/src/frontend/src/tokenController.js index 6d1b4b7a7fa311f3dbc6032ce5f11c282f71dc09..fcbdc07fb7d8cf05c56b9070b5339691c7b2819e 100644 --- a/FullstackProsjekt/src/frontend/src/tokenController.js +++ b/FullstackProsjekt/src/frontend/src/tokenController.js @@ -1,3 +1,5 @@ +import {apiClient} from "@/api.js"; + export const getToken = () => { return localStorage.getItem('token'); } @@ -8,4 +10,9 @@ export const setToken = (token) => { export const removeToken = () => { localStorage.removeItem('token'); +} + +export const getIdByToken= () => { + //TODO: set up encryption and getID + return apiClient.get('/user/getId/' + getToken()); } \ No newline at end of file diff --git a/FullstackProsjekt/src/frontend/src/views/EditQuizView.vue b/FullstackProsjekt/src/frontend/src/views/EditQuizView.vue index 1878a53587c38d383e5b6a3733edd57a1f12bfae..0d3e9164a69cecf8d2cfe0ed4006bd559215240c 100644 --- a/FullstackProsjekt/src/frontend/src/views/EditQuizView.vue +++ b/FullstackProsjekt/src/frontend/src/views/EditQuizView.vue @@ -1,26 +1,81 @@ -<script setup> +<script> import NewQuestionModel from "@/components/shared/NewQuestionModel.vue"; -import {ref} from "vue"; +import { ref, onMounted } from 'vue'; +import { useRoute } from 'vue-router'; import router from "@/router/index.js"; import {apiClient} from "@/api.js"; +import QuestionCard from "@/components/shared/QuestionCard.vue"; +/** const quizId = ref(null); //TODO: set quiz id when routing here + const createdQuestion = ref(null); const newAnswers = ref([]); -const showNewQuestionModal = ref(false); +let showNewQuestionModal = ref(false); const selectedAnswer = ref(null); const existingQuestions = ref([]); let answerId = 1; -const errorMsg = ''; //TODO: display error to user - +const quizName = ref(''); +const errorMsg = ''; //TODO: display error to user*/ +//send list of question-ids? +export default { + components: {NewQuestionModel, QuestionCard}, + data() { + return { + showNewQuestion: false, + creatorId: null, + quizId: null, + quizTitle: '', + questions: [], //question list is just a list of q-ids!! + category: '', + difficulty: '', + errorMsg: '' + //TODO: make quiz object + }; + }, + mounted() { + this.quizId = this.$route.params.quizId; + this.getQuiz(this.quizId); + }, + methods: { + getQuiz(quizId) { + console.log('Fetching data for quiz: ', quizId); + try { + apiClient.get('/quiz/quiz/' + this.quizId).then(response => { + this.quizTitle = JSON.parse(response.data.title); + this.questions = JSON.parse(JSON.stringify(response.data.questions)); + this.creatorId = JSON.parse(response.data.creatorId); + this.category = JSON.parse(response.data.category); + this.difficulty = JSON.parse(response.data.difficulty); + }); + } catch (error) { + //TODO: proper error handling + this.errorMsg = 'Error retrieving quizzes'; + } + }, + newQuestion() { + this.showNewQuestion = true; + console.log(this.showNewQuestion); + }, + hideNewQuestion() { + this.showNewQuestion = false; + //TODO: questions answers, +question count + }, + deleteQuiz() { + //API req, quizId + } + }, +}; +/** function createQuestion() { showNewQuestionModal.value = true; } function destroyModal() { showNewQuestionModal.value = false; } + function addNewAnswers() { const newAnswer = { id: answerId++, @@ -38,8 +93,9 @@ function handleRadioToggle(Id) { answer.correct_answer = 0 } }) -} +}*/ +/** function validateAnswers() { for(const answer of newAnswers.value) { if(answer.answer.trim()==='') { @@ -55,7 +111,7 @@ function answerCount() { return true } } - +/* async function getExistingQuestions() { try { const response = await apiClient.get(`/quizzes/${quizId.value}/questions`); // Fetch questions for a specific quiz @@ -66,7 +122,9 @@ async function getExistingQuestions() { alert('An error occurred while fetching existing questions'); } } +*/ +/** async function submitQuestion() { //TODO: proper error handling if(!createdQuestion.value){ @@ -90,15 +148,21 @@ async function submitQuestion() { this.errorMsg = 'Error logging in'; } } - +*/ </script> <template> <body> - <div class="createQuestion-page"> + <div class="newQuizDiv"> <router-link to="/overviewQuiz"> <- </router-link> - <h1>Create a question to your quiz</h1> + <h1>Edit quiz {{quizId}}</h1> + <div class="question-div"> + <QuestionCard v-for="question in questions" :question-id=question.id + :key="question.id"/> + </div> + <NewQuestionModel v-if="showNewQuestion" @close="hideNewQuestion" quiz-id="this.quizId"/> + <!-- <div class="question-table"> <table class="table"> <thead> @@ -121,6 +185,9 @@ async function submitQuestion() { </tbody> </table> + --> + <!-- + <Teleport to="body"> <NewQuestionModel :show="showNewQuestionModal" @close="destroyModal"> @@ -166,11 +233,11 @@ async function submitQuestion() { </NewQuestionModel> </Teleport> - - </div> - - <div> - <button @click="createQuestion" class="add-Btn"> Add Question </button> <br> + </div> + --> + <div class="footer"> + <button @click="newQuestion" class="add-Btn"> Add Question </button> + <button class="delete-btn"> DELETE QUIZ </button> <button class="save-Btn"> SAVE QUIZ </button> </div> </div> @@ -179,7 +246,7 @@ async function submitQuestion() { </template> <style> -.createQuestion-page{ +.newQuizDiv{ padding: 20px; } diff --git a/FullstackProsjekt/src/frontend/src/views/NewQuizView.vue b/FullstackProsjekt/src/frontend/src/views/NewQuizView.vue new file mode 100644 index 0000000000000000000000000000000000000000..6f60f8f45ff84da03c7dde5c7ad99f85173dcb4c --- /dev/null +++ b/FullstackProsjekt/src/frontend/src/views/NewQuizView.vue @@ -0,0 +1,199 @@ +<script> +import NewQuestionModel from "@/components/shared/NewQuestionModel.vue"; +import { ref, onMounted } from 'vue'; +import { useRoute } from 'vue-router'; +import router from "@/router/index.js"; +import {apiClient} from "@/api.js"; +import {getIdByToken} from "@/tokenController.js"; +import {categoryEnums} from "@/data/categories.js" +import {difficultyEnums} from "@/data/difficulties.js"; + +//like editquiz, but w/o questions, redirect to edit when quiz is constructed + +export default { + data() { + return { + showNewQuestion: false, + creatorId: null, + quiz: null, + quizId: null, + quizTitle: '', + questions: [], + category: '', + difficulty: '', + errorMsg: '', + selectedCategory: null, + categories: categoryEnums, + selectedDifficulty: null, + difficulties: difficultyEnums, + //TODO: make quiz object + }; + }, + mounted() { + this.getUser(); + }, + methods: { + async constructQuiz() { + try { + await apiClient.post('quiz/create', { + title: this.quizTitle, + questionIds: this.questions, + creatorId: this.creatorId, + category: this.selectedCategory, + difficulty: this.selectedDifficulty + }).then(response => { + this.quizId = JSON.parse(response.data.id); + router.push({name: 'editQuiz', params: {quizId: this.quizId}}); + }) + } catch(error){ + this.errorMsg = 'Cannot construct quiz'; + } + }, + getUser() { + this.creatorId = getIdByToken(); + } + + }, +} +</script> + +<template> + <body> + <form @submit.prevent="constructQuiz"> + <div class="newQuizDiv"> + <router-link to="/overviewQuiz"> <- </router-link> + <h1>New quiz</h1> + <div> + <h2>Title</h2> + <input> + </div> + <div> + <h2>Category</h2> + <form> + <select v-model="selectedCategory"> + <option v-for="category in categories" :key="category.id" :value="category.id">{{category.category}}</option> + </select> + </form> + </div> + <div> + <h2>Difficulty</h2> + <form> + <select v-model="selectedDifficulty"> + <option v-for="difficulty in difficulties" :key="difficulty.id" :value="difficulty.id">{{difficulty.difficulty}}</option> + </select> + </form> + </div> + <!-- + <div class="question-table"> + <table class="table"> + <thead> + <tr> + <th scope="col">#</th> + <th scope="col">Question</th> + <th scope="col">Action</th> + </tr> + </thead> + <tbody> + <tr> + <th scope="row">1</th> + <td>What is Vue?</td> + <td> + <button class="play-btn">View</button> + <button class="edit-btn">Edit</button> + <button class="delete-btn"> Delete</button> + </td> + </tr> + + </tbody> + </table> + --> + <!-- + + + <Teleport to="body"> + <NewQuestionModel :show="showNewQuestionModal" @close="destroyModal"> + + <template #header> + <h5> Add New Question</h5> + </template> + + <template #body> + <form> + <div class="mb-3"> + <label for="question" class="form-label">Question</label> <br> + <input type="text" v-model="createdQuestion" class="form-control" id="questionInput"> + </div> + <table class="table"> + <thead> + <tr> + <th scope="col">#</th> + <th scope="col">Answer</th> + <th scope="col">Correct ?</th> + </tr> + </thead> + <tbody> + <tr v-for="(answer, index) in newAnswers"> + <th scope="row">{{answer.id}}</th> + <td> + <input type="text" v-model="answer.answer" id="questionInput"> + </td> + <td> + <input :checked="answer.correct_answer === 1" class="form-check-input" :value="answer.id" @change="handleRadioToggle(answer.id)" type="radio"> + </td> + </tr> + </tbody> + </table> + </form> + </template> + + <template #footer> + <button @click="addNewAnswers" class="add-Btn" v-if="newAnswers.length<4" >+</button> + <button @click="destroyModal" class="close-btn"> Close</button> + <button v-if="newAnswers.length>=2" @click="submitQuestion" class="submit-btn">Submit</button> + </template> + + </NewQuestionModel> + </Teleport> + </div> + --> + <div class="footer"> + <router-link to="/overviewQuiz"> Cancel </router-link> + <div class="submit-section"> + <input id="submit" type="submit"/> + </div> + </div> + </div> + </form> + </body> + +</template> + +<style> +.newQuizDiv{ + padding: 20px; +} + +input{ + height: 25px; + width: 100%; +} + +.submit-section { + display: flex; + justify-content: center; + align-items: center; +} + +#submit { + min-width: 150px; + min-height: 60px; + font-size: 24px; + border-radius: 6px; + background-color: #242F40; + color: white; + border: none; + cursor: pointer; + margin-top: 20px; +} + +</style> \ No newline at end of file diff --git a/FullstackProsjekt/src/frontend/src/views/OverviewQuizView.vue b/FullstackProsjekt/src/frontend/src/views/OverviewQuizView.vue index b78c5f8d992fdd6dca60c3a317c0daace8fab03a..015609bef2c36c39405adaa9859f847a7afc5a3c 100644 --- a/FullstackProsjekt/src/frontend/src/views/OverviewQuizView.vue +++ b/FullstackProsjekt/src/frontend/src/views/OverviewQuizView.vue @@ -1,98 +1,74 @@ <template> <body> <div class="overViewQuestion-page"> - <router-link to="/dashboard"> <- </router-link> - <h1>Your quizzes</h1> - <p>Select a quiz for your creation to either play, edit or delete</p> + <div class="headerDiv"> + <div> + <router-link to="/dashboard"> <- </router-link> + <h1>Your quizzes</h1> + <p>Select a quiz for your creation to either play, edit or delete</p> + </div> + <div> + <button class="add-Btn">Create Quiz</button> + <router-link to="/createQuiz"> New quiz </router-link> + </div> + </div> - <div class="row"> - <div class="course-col"> - - <div class="quiz-header"> - <h3>Quiz 1</h3> - <Svg :name="selectedIcon" /> - </div> - - <div class="quiz-body"> - <p>Info about quiz...</p> - <p>Category:</p> - <div id="form-box"> - <form> - <select id="quiz-category-1" @change="changeCategory"> - <option value="Animal">Animals</option> - <option value="Athletic">Athletic/Sport</option> - <option value="Computer" selected="selected">Computer Science</option> - <option value="Drama">Drama/Movie</option> - <option value="Music">Music</option> - <option value="Religion">Religion</option> - <option value="Science">Science</option> - <option value="Society">Society</option> - <option value="Other">Other</option> - </select> - </form> - </div> - </div> - - <div class="quiz-footer"> - <router-link to="/play-quiz" class="play-btn">Play</router-link> - <router-link to="/createQuiz" class="edit-btn">Edit</router-link> - <button class="delete-btn">Delete</button> - </div> - </div> - - <div> - <button class="add-Btn">Create Quiz</button> - </div> + <div class="row"> + <div class="quiz-div"> + <QuizCard id="quizCard" v-for= "quiz in quizList" :key="quiz.id" :quiz-id="quiz.id" /> + </div> </div> </div> </body> </template> <script> -import { defineComponent } from "vue"; -import Svg from "@/assets/Svg.vue"; +import QuizCard from "@/components/shared/QuizCard.vue"; +import {getIdByToken} from "@/tokenController.js"; import {apiClient} from "@/api.js"; -import {ref} from "vue"; -getQuizzes(); -const quizzes = ref([]); - -async function getQuizzes() { - //TODO: try/catch - const response = await apiClient.get('/quizzes/${quizId.value}'); - quizzes.value = response.data; //TODO: create parsing method -} - -export default defineComponent({ - components: { Svg }, +export default { + components: { + QuizCard, + }, data() { return { - selectedCategory: 'Computer', - categoryIcons: { - 'Animal': 'animal-category', - 'Athletic': 'athletic-category', - 'Computer': 'computer-category', - 'Drama': 'drama-category', - 'Music': 'music-category', - 'Religion': 'religion-category', - 'Science': 'science-category', - 'Society': 'society-category', - 'Other': 'other-category' - } + userId: null, + quizNo: 0, + quizList: [], }; }, - computed: { - selectedIcon() { - return this.categoryIcons[this.selectedCategory]; - } + mounted() { + this.setUserId(); + this.populateQuizzes(); + }, + methods: { + + async populateQuizzes() { + try { + await apiClient.get('/quiz/creator/' + this.userId).then(response => { + this.quizList = JSON.parse(JSON.stringify(response.data)); + //TODO: set max amt. + this.quizNo = this.quizList.length; + }); + } catch (error) { + //TODO: proper error handling + this.errorMsg = 'Error retrieving quizzes'; + } + },/* + populateQuizzes() { + this.quizNo = this.quizList.length; + },*/ + setUserId() { + this.userId = getIdByToken(); + } }, - methods: { - changeCategory(event) { - this.selectedCategory = event.target.value; - } - } -}); + created() { + this.setUserId(); + this.populateQuizzes(); + } +} </script> <style> @@ -100,26 +76,22 @@ export default defineComponent({ padding: 50px; } -.quiz-header{ - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; -} -.quiz-body{ - margin-bottom: 40px; +.quiz-div { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + float: left; + box-sizing: border-box; } -#quiz-category{ - padding: 1px; - background-color: #FFF; - border-radius: 5px; - font-family: monospace; + +.headerDiv { + display: flex; + width: 100%; + justify-content: space-between; } -.quiz-footer{ - display: flex; - justify-content: space-evenly; - align-items: center; - padding-left: 25px; - padding-right: 25px; + +#quizCard { + width: calc(50% - 10px); } + </style> \ No newline at end of file