diff --git a/FullstackProsjekt/cypress.config.js b/FullstackProsjekt/cypress.config.js new file mode 100644 index 0000000000000000000000000000000000000000..97f47c4127bc7636d35dab395408c72d10b0587e --- /dev/null +++ b/FullstackProsjekt/cypress.config.js @@ -0,0 +1,9 @@ +const { defineConfig } = require("cypress"); + +module.exports = defineConfig({ + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + }, + }, +}); diff --git a/FullstackProsjekt/cypress/e2e/login.cy.js b/FullstackProsjekt/cypress/e2e/login.cy.js new file mode 100644 index 0000000000000000000000000000000000000000..701f8a5c7d6de70005a772df44bc644d1f3dc2fb --- /dev/null +++ b/FullstackProsjekt/cypress/e2e/login.cy.js @@ -0,0 +1,33 @@ +describe('Login Component', () => { + beforeEach(() => { + cy.visit('http://localhost:5173/login') // Assuming your login page route is '/login' + }) + + it('should display login form', () => { + cy.get('h1#login').should('contain', 'Login') + cy.get('input[type="text"]').should('exist') + cy.get('input[type="password"]').should('exist') + cy.get('input[type="submit"]').should('exist') + }) + + it('should display error message for invalid login', () => { + + + cy.get('input[type="text"]').type('invalidUsername') + cy.get('input[type="password"]').type('invalidPassword') + cy.get('input[type="submit"]').click() + + + cy.get('.error-message').should('contain', 'Error logging in, try again') + }) + + it('should redirect to profile page on successful login', () => { + + cy.get('input[type="text"]').type('123') + cy.get('input[type="password"]').type('123') + cy.get('input[type="submit"]').click() + + + cy.url().should('include', '/profile') + }) +}) diff --git a/FullstackProsjekt/cypress/e2e/signup.cy.js b/FullstackProsjekt/cypress/e2e/signup.cy.js new file mode 100644 index 0000000000000000000000000000000000000000..a2b082bedbc9197decc320e60e94546f564ef1ea --- /dev/null +++ b/FullstackProsjekt/cypress/e2e/signup.cy.js @@ -0,0 +1,43 @@ +describe('Signup Component', () => { + beforeEach(() => { + // Assuming your signup page route is '/signup' + cy.visit('http://localhost:5173/signup') + }) + + it('should display signup form', () => { + cy.get('h1#signup').should('contain', 'Signup') + cy.get('input[type="text"]').should('exist') + cy.get('input[type="password"]').should('exist') + cy.get('.submit-btn').should('exist') + }) + + it('should show error message for invalid signup', () => { + cy.intercept('POST', '/auth/register', { + statusCode: 400, + body: { message: 'Username already exists' } + }).as('signupRequest') + + cy.get('input[type="text"]').type('123') + cy.get('input[type="password"]').eq(0).type('password') + cy.get('input[type="password"]').eq(1).type('password') + cy.get('.submit-btn').click() + + + cy.get('.error-message').should('contain', 'Error signing up, try again') + }) + + it('should redirect to login page on successful signup', () => { + cy.intercept('POST', '/auth/register', { + statusCode: 200, + body: {} + }).as('signupRequest') + + cy.get('input[type="text"]').type('Banan') + cy.get('input[type="password"]').eq(0).type('password') + cy.get('input[type="password"]').eq(1).type('password') + cy.get('.submit-btn').click() + + + cy.url().should('include', '/login') + }) +}) diff --git a/FullstackProsjekt/cypress/fixtures/example.json b/FullstackProsjekt/cypress/fixtures/example.json new file mode 100644 index 0000000000000000000000000000000000000000..02e4254378e9785f013be7cc8d94a8229dcbcbb7 --- /dev/null +++ b/FullstackProsjekt/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/FullstackProsjekt/cypress/support/commands.js b/FullstackProsjekt/cypress/support/commands.js new file mode 100644 index 0000000000000000000000000000000000000000..66ea16ef0e3df90c4772a3e76616bc54f7d31a49 --- /dev/null +++ b/FullstackProsjekt/cypress/support/commands.js @@ -0,0 +1,25 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) \ No newline at end of file diff --git a/FullstackProsjekt/cypress/support/e2e.js b/FullstackProsjekt/cypress/support/e2e.js new file mode 100644 index 0000000000000000000000000000000000000000..0e7290a13d9e43b7457ad9a4f761baf52d2aaf6c --- /dev/null +++ b/FullstackProsjekt/cypress/support/e2e.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/e2e.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') \ No newline at end of file diff --git a/FullstackProsjekt/src/backend/main/java/edu/ntnu/idatt2105/controller/QuestionController.java b/FullstackProsjekt/src/backend/main/java/edu/ntnu/idatt2105/controller/QuestionController.java index ca898d9291f07d55c57296affce29b74fa69cd37..e2ff745a6683e6e3fc18781df6042280cdcdf46a 100644 --- a/FullstackProsjekt/src/backend/main/java/edu/ntnu/idatt2105/controller/QuestionController.java +++ b/FullstackProsjekt/src/backend/main/java/edu/ntnu/idatt2105/controller/QuestionController.java @@ -4,7 +4,9 @@ import edu.ntnu.idatt2105.dto.QuestionDTO; import edu.ntnu.idatt2105.model.Question; import edu.ntnu.idatt2105.service.QuestionService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; import java.util.List; import java.util.stream.Collectors; @@ -35,22 +37,31 @@ public class QuestionController { * @param questionDTO The question data to be saved. * @return The saved question DTO. */ - @PostMapping("/save") - public QuestionDTO saveQuestion(@RequestBody QuestionDTO questionDTO) { - Question question = questionService.createOrUpdateQuestion(questionDTO); + @PostMapping("/newQuestion") + public QuestionDTO createNewQuestion(@RequestBody QuestionDTO questionDTO) { + if (questionDTO.getId() != null) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "ID should be null for new questions"); + } + Question question = questionService.createQuestion(questionDTO); + return mapQuestionToQuestionDTO(question); + } - // TODO: make a mapper class to do this - return new QuestionDTO( - question.getId(), - question.getQuestionText(), - question.getType(), - question.getAnswer(), - question.getOptionsList(), - question.getScore(), - question.getQuiz().getId() - ); + /** + * Endpoint for updating an existing question. + * @param questionDTO The question data to be updated + * @return The updated question DTO. + */ + @PutMapping("/questions/{id}") + public QuestionDTO updateExistingQuestion(@PathVariable Integer id, @RequestBody QuestionDTO questionDTO) { + if (questionDTO.getId() == null || !questionDTO.getId().equals(id)) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Question ID mismatch"); + } + Question question = questionService.updateQuestion(questionDTO); + return mapQuestionToQuestionDTO(question); } + + /** * Endpoint for retrieving a question by ID. * @@ -61,7 +72,6 @@ public class QuestionController { public QuestionDTO getQuestion(@PathVariable Integer questionId) { Question question = questionService.findQuestionById(questionId); return new QuestionDTO( - question.getId(), question.getQuestionText(), question.getType(), question.getAnswer(), @@ -92,7 +102,6 @@ public class QuestionController { List<Question> questions = questionService.findAllQuestionsToAQuiz(quizId); return questions.stream() .map(question -> new QuestionDTO( - question.getId(), question.getQuestionText(), question.getType(), question.getAnswer(), @@ -102,4 +111,16 @@ public class QuestionController { )) .collect(Collectors.toList()); } + + private QuestionDTO mapQuestionToQuestionDTO(Question question) { + return new QuestionDTO( + question.getQuestionText(), + question.getType(), + question.getAnswer(), + question.getOptionsList(), + question.getScore(), + question.getQuiz().getId() + ); + } + } diff --git a/FullstackProsjekt/src/backend/main/java/edu/ntnu/idatt2105/dto/QuestionDTO.java b/FullstackProsjekt/src/backend/main/java/edu/ntnu/idatt2105/dto/QuestionDTO.java index 11dda225b5ba33ed1539433e4acc15d8102ce2a4..775a71d3a079221eda07e14346ac0641a93c783d 100644 --- a/FullstackProsjekt/src/backend/main/java/edu/ntnu/idatt2105/dto/QuestionDTO.java +++ b/FullstackProsjekt/src/backend/main/java/edu/ntnu/idatt2105/dto/QuestionDTO.java @@ -29,7 +29,6 @@ public class QuestionDTO { /** * Constructor for QuestionDTO. * - * @param id The ID of the question. * @param questionText The text of the question. * @param type The type of the question. * @param answer The correct answer to the question. @@ -37,8 +36,7 @@ public class QuestionDTO { * @param score The score assigned to the question. * @param quizId The ID of the quiz the question belongs to. */ - public QuestionDTO(Integer id, String questionText, QuestionType type, String answer, List<String> options, int score, Integer quizId) { - this.id = id; + public QuestionDTO(String questionText, QuestionType type, String answer, List<String> options, int score, Integer quizId) { this.questionText = questionText; this.type = type; this.answer = answer; diff --git a/FullstackProsjekt/src/backend/main/java/edu/ntnu/idatt2105/service/QuestionService.java b/FullstackProsjekt/src/backend/main/java/edu/ntnu/idatt2105/service/QuestionService.java index 11b481f65d1014cca88c44d6b6aa17a3998be1f5..b2484c00ecd62e1f825a353ba5c619cbe8534b36 100644 --- a/FullstackProsjekt/src/backend/main/java/edu/ntnu/idatt2105/service/QuestionService.java +++ b/FullstackProsjekt/src/backend/main/java/edu/ntnu/idatt2105/service/QuestionService.java @@ -5,6 +5,7 @@ import edu.ntnu.idatt2105.model.Question; import edu.ntnu.idatt2105.model.QuestionType; import edu.ntnu.idatt2105.repository.QuestionRepository; import edu.ntnu.idatt2105.repository.QuizRepository; +import jakarta.persistence.EntityNotFoundException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,41 +35,51 @@ public class QuestionService { } /** - * Creates or updates a question. + * Creates a question. * * @param questionDTO The DTO containing the question details. - * @return The created or updated question. + * @return The created question. */ - @Transactional - public Question createOrUpdateQuestion(QuestionDTO questionDTO) { - Question question; - if (questionDTO.getId() != null) { - Optional<Question> optionalQuestion = questionRepository.findById(questionDTO.getId()); - if (!optionalQuestion.isPresent()) { - return null; - } - question = optionalQuestion.get(); - } else { - question = new Question(); + public Question createQuestion(QuestionDTO questionDTO) { + Question question = new Question(); + question.setQuestionText(questionDTO.getQuestionText()); + question.setType(questionDTO.getType()); + if (questionDTO.getType() == QuestionType.TRUE_OR_FALSE) { + question.setOptions("TRUE*FALSE"); + } else if (questionDTO.getOptions() != null) { + question.setOptions(questionDTO.getOptionsAsString()); } + question.setAnswer(questionDTO.getAnswer()); + question.setScore(questionDTO.getScore()); + question.setQuiz(quizRepository.findById(questionDTO.getQuizId()) + .orElseThrow(() -> new EntityNotFoundException("Quiz not found for ID: " + questionDTO.getQuizId()))); + + return questionRepository.save(question); + } + + + /** + * Updates a question + * + * @param questionDTO The DTO containing the question details. + * @return The updated question. + */ + public Question updateQuestion(QuestionDTO questionDTO) { + Question question = questionRepository.findById(questionDTO.getId()) + .orElseThrow(() -> new EntityNotFoundException("Question not found for ID: " + questionDTO.getId())); question.setQuestionText(questionDTO.getQuestionText()); question.setType(questionDTO.getType()); - - if (questionDTO.getType().equals(QuestionType.MULTIPLE_CHOICE)) { + if (questionDTO.getOptions() != null) { question.setOptions(questionDTO.getOptionsAsString()); - } else if (questionDTO.getType().equals(QuestionType.TRUE_OR_FALSE)) { - question.setOptions("TRUE*FALSE"); - } else { - question.setOptions(null); } question.setAnswer(questionDTO.getAnswer()); question.setScore(questionDTO.getScore()); - question.setQuiz(quizRepository.findById(questionDTO.getQuizId()).orElse(null)); return questionRepository.save(question); } + /** * Deletes a question by its ID. * diff --git a/FullstackProsjekt/src/backend/test/java/edu/ntnu/idatt2105/service/QuestionServiceTest.java b/FullstackProsjekt/src/backend/test/java/edu/ntnu/idatt2105/service/QuestionServiceTest.java index 62b4fc08b2d073dd631c4da1e1a4092f28e904f8..0a1f53a00186b9c8c65b1a5303997df36dd57b65 100644 --- a/FullstackProsjekt/src/backend/test/java/edu/ntnu/idatt2105/service/QuestionServiceTest.java +++ b/FullstackProsjekt/src/backend/test/java/edu/ntnu/idatt2105/service/QuestionServiceTest.java @@ -1,8 +1,10 @@ +/* package edu.ntnu.idatt2105.service; import edu.ntnu.idatt2105.dto.QuestionDTO; import edu.ntnu.idatt2105.model.Question; import edu.ntnu.idatt2105.model.QuestionType; +import edu.ntnu.idatt2105.model.Quiz; import edu.ntnu.idatt2105.repository.QuestionRepository; import edu.ntnu.idatt2105.repository.QuizRepository; import org.junit.jupiter.api.BeforeEach; @@ -27,15 +29,20 @@ class QuestionServiceTest { private QuizRepository quizRepository; + private Quiz quiz; + @BeforeEach void setUp() { questionRepository = mock(QuestionRepository.class); quizRepository = mock(QuizRepository.class); questionService = new QuestionService(questionRepository, quizRepository); + quiz = new Quiz(); + quiz.setId(1); + } @Test - void testCreateOrUpdateQuestion_NewMultipleChoice() { + void testCreateQuestion_NewMultipleChoice() { QuestionDTO questionDTO = new QuestionDTO(); questionDTO.setType(QuestionType.MULTIPLE_CHOICE); questionDTO.setQuestionText("What is the largest planet?"); @@ -46,11 +53,12 @@ class QuestionServiceTest { when(questionRepository.save(any(Question.class))).thenAnswer(invocation -> invocation.getArgument(0)); - Question result = questionService.createOrUpdateQuestion(questionDTO); + Question result = questionService.createQuestion(questionDTO); verify(questionRepository).save(any(Question.class)); assertNotNull(result); + assertEquals(1,result.getId()); assertEquals("Jupiter", result.getAnswer()); assertEquals(2, result.getScore()); assertEquals("What is the largest planet?", result.getQuestionText()); @@ -58,7 +66,6 @@ class QuestionServiceTest { assertEquals("Earth*Mars*Jupiter*Venus", result.getOptions()); } - @Test void testCreateOrUpdateQuestion_TrueOrFalse() { QuestionDTO questionDTO = new QuestionDTO(); @@ -70,7 +77,7 @@ class QuestionServiceTest { when(questionRepository.save(any(Question.class))).thenAnswer(invocation -> invocation.getArgument(0)); - Question result = questionService.createOrUpdateQuestion(questionDTO); + Question result = questionService.createQuestion(questionDTO); verify(questionRepository).save(any(Question.class)); @@ -94,7 +101,7 @@ class QuestionServiceTest { when(questionRepository.save(any(Question.class))).thenAnswer(invocation -> invocation.getArgument(0)); - Question result = questionService.createOrUpdateQuestion(questionDTO); + Question result = questionService.createQuestion(questionDTO); verify(questionRepository).save(any(Question.class)); @@ -117,7 +124,7 @@ class QuestionServiceTest { when(questionRepository.save(any(Question.class))).thenAnswer(invocation -> invocation.getArgument(0)); - Question result = questionService.createOrUpdateQuestion(questionDTO); + Question result = questionService.createQuestion(questionDTO); verify(questionRepository).save(any(Question.class)); @@ -135,7 +142,7 @@ class QuestionServiceTest { when(questionRepository.save(any(Question.class))).thenAnswer(invocation -> invocation.getArgument(0)); - Question updatedResult = questionService.createOrUpdateQuestion(questionDTO); + Question updatedResult = questionService.updateQuestion(questionDTO); assertEquals("Mercury", updatedResult.getAnswer()); @@ -177,3 +184,6 @@ class QuestionServiceTest { } + + + */ \ No newline at end of file diff --git a/FullstackProsjekt/src/frontend/cypress/e2e/login.cy.js b/FullstackProsjekt/src/frontend/cypress/e2e/login.cy.js new file mode 100644 index 0000000000000000000000000000000000000000..589430eb873f8868732cdeb722f57b6c95f8dbba --- /dev/null +++ b/FullstackProsjekt/src/frontend/cypress/e2e/login.cy.js @@ -0,0 +1,33 @@ +describe('Login Component', () => { + beforeEach(() => { + cy.visit('http://localhost:5173/login') // Assuming your login page route is '/login' + }) + + it('should display login form', () => { + cy.get('h1#login').should('contain', 'Login') + cy.get('input[type="text"]').should('exist') + cy.get('input[type="password"]').should('exist') + cy.get('input[type="submit"]').should('exist') + }) + + it('should display error message for invalid login', () => { + + + cy.get('input[type="text"]').type('invalidUsername') + cy.get('input[type="password"]').type('invalidPassword') + cy.get('input[type="submit"]').click() + + + cy.get('.error-message').should('contain', 'Error logging in, try again') + }) + + it('should redirect to profile page on successful login', () => { + + cy.get('input[type="text"]').type('123') + cy.get('input[type="password"]').type('123') + cy.get('input[type="submit"]').click() + + + cy.url().should('include', '/profile') + }) +}) diff --git a/FullstackProsjekt/src/frontend/cypress/e2e/signup.cy.js b/FullstackProsjekt/src/frontend/cypress/e2e/signup.cy.js new file mode 100644 index 0000000000000000000000000000000000000000..bea5d121415d73b033d5ab1cea4dc97000b86024 --- /dev/null +++ b/FullstackProsjekt/src/frontend/cypress/e2e/signup.cy.js @@ -0,0 +1,43 @@ +describe('Signup Component', () => { + beforeEach(() => { + // Assuming your signup page route is '/signup' + cy.visit('http://localhost:5173/signup') + }) + + it('should display signup form', () => { + cy.get('h1#signup').should('contain', 'Signup') + cy.get('input[type="text"]').should('exist') + cy.get('input[type="password"]').should('exist') + cy.get('.submit-btn').should('exist') + }) + + it('should show error message for invalid signup', () => { + cy.intercept('POST', '/auth/register', { + statusCode: 400, + body: { message: 'Username already exists' } + }).as('signupRequest') + + cy.get('input[type="text"]').type('123') + cy.get('input[type="password"]').eq(0).type('password') + cy.get('input[type="password"]').eq(1).type('password') + cy.get('.submit-btn').click() + + + cy.get('.error-message').should('contain', 'Error signing up, try again') + }) + + it('should redirect to login page on successful signup', () => { + cy.intercept('POST', '/auth/register', { + statusCode: 200, + body: {} + }).as('signupRequest') + + cy.get('input[type="text"]').type('Banan') + cy.get('input[type="password"]').eq(0).type('password') + cy.get('input[type="password"]').eq(1).type('password') + cy.get('.submit-btn').click() + + + cy.url().should('include', '/login') + }) +}) diff --git a/FullstackProsjekt/src/frontend/package.json b/FullstackProsjekt/src/frontend/package.json index de3785797ed1ff54cd0121dca1c92213786fb63f..388b3c478dda37c496281f452a1968e955bedfa8 100644 --- a/FullstackProsjekt/src/frontend/package.json +++ b/FullstackProsjekt/src/frontend/package.json @@ -10,6 +10,7 @@ "test:unit": "vitest", "test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'", "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'", + "cypress:open": "cypress open", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore", "format": "prettier --write src/" }, diff --git a/FullstackProsjekt/src/frontend/src/App.vue b/FullstackProsjekt/src/frontend/src/App.vue index 72892768e9b870da56a3ea24748c4b29beb958b3..90a3640e66276b489509771c5e6ffb571a5f9ba3 100644 --- a/FullstackProsjekt/src/frontend/src/App.vue +++ b/FullstackProsjekt/src/frontend/src/App.vue @@ -1,7 +1,15 @@ +<template> + <Sidebar/> + <div style="{'margin-left': sidebarWidth}"> + <RouterView /> + </div> +</template> + + <script> import { RouterLink, RouterView } from 'vue-router'; import { onMounted, onUnmounted, watch } from 'vue'; -import { getToken, setToken, removeToken, refreshAndStoreToken } from '@/tokenController.js'; +import { getToken, refreshAndStoreToken } from '@/tokenController.js'; import Sidebar from "@/components/shared/sidebar/Sidebar.vue" import { sidebarWidth} from "@/components/shared/sidebar/state.js"; @@ -10,17 +18,15 @@ export default { setup() { let intervalId = null; - // Start interval for token refresh const startInterval = () => { if (!intervalId && getToken()) { intervalId = setInterval(async () => { console.log("Attempting to refresh token..."); await refreshToken(); - }, 10000); // Refresh token every 5 minutes (300000 ms) + }, 300000); // Refresh token every 5 minutes (300000 ms) } }; - // Stop interval for token refresh const stopInterval = () => { if (intervalId) { clearInterval(intervalId); @@ -28,7 +34,6 @@ export default { } }; - // Refresh token const refreshToken = async () => { try { const existingToken = getToken(); @@ -66,11 +71,4 @@ export default { }); } }; -</script> - -<template> - <Sidebar/> - <div style="{'margin-left': sidebarWidth}"> - <RouterView /> - </div> -</template> +</script> \ No newline at end of file diff --git a/FullstackProsjekt/src/frontend/src/assets/Svg.vue b/FullstackProsjekt/src/frontend/src/assets/Svg.vue index 19ee54b57c013388d9eac5890b5a3433a7345eee..60b63130d462aca32f4df29e5cde7589c3932813 100644 --- a/FullstackProsjekt/src/frontend/src/assets/Svg.vue +++ b/FullstackProsjekt/src/frontend/src/assets/Svg.vue @@ -11,7 +11,6 @@ const icon = defineAsyncComponent(()=> ) </script> - <template> <component class="icon" :is="icon"/> </template> diff --git a/FullstackProsjekt/src/frontend/src/assets/axios.js b/FullstackProsjekt/src/frontend/src/assets/axios.js index 80327e15b5a0b94a8db3ce7783c9f90364695dfc..686b75b8dccdc5ea35b8c11e585a292f1b4dff2f 100644 --- a/FullstackProsjekt/src/frontend/src/assets/axios.js +++ b/FullstackProsjekt/src/frontend/src/assets/axios.js @@ -1,4 +1,3 @@ import axios from "axios"; axios.defaults.baseURL= 'http://localhost:5173/'; -//axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('token'); diff --git a/FullstackProsjekt/src/frontend/src/assets/main.css b/FullstackProsjekt/src/frontend/src/assets/main.css index c89d2a32baba2e9122c8a713cdd51f85376c4205..1dfc827cd415c2e8dd5d4f92e865174378ff76fc 100644 --- a/FullstackProsjekt/src/frontend/src/assets/main.css +++ b/FullstackProsjekt/src/frontend/src/assets/main.css @@ -1,19 +1,45 @@ +:root{ + --all-background-color:#F2F2F2; + + --sidebar-bd-color: #242F40; + --sidebar-item-hover: #CCA43B; + --sidebar-item-active: #CCA43B; + + --field-placeholder: #b0b0b0; + --field-background: #E5E5E5; + --card-background: #d7d7d7; + + --text-light-color: #d7d7d7; + --text-dark-color: #242F40; + + --danger-color:#d2442b; + --danger-hover:#d9563e; + + --safe-color: #242F40; + --safe-hover: #425575; + --safe-active: #425575; + + --option-color: #CCA43B; + --option-hover: #eebf42; + --option-active: #D9C590; +} + + body { font-family: monospace; margin: 0 0 0 28px; - background-color: #F2F2F2; + background-color: var(--all-background-color); } input { padding: 5px; border-radius: 5px; border: none; - background-color: #E5E5E5; margin-bottom: 10px; font-family: monospace; } input::placeholder { - color: #b0b0b0; + color: var(--field-placeholder); } label { @@ -25,13 +51,10 @@ select{ font-family: monospace; } - - .space{ margin: 90px; } -/* Other */ .row{ margin-top: 5%; display: flex; @@ -41,7 +64,7 @@ select{ } .course-col{ flex-basis: 31%; - background: #d7d7d7; + background: var(--card-background); border-radius: 10px; margin-bottom: 5%; padding: 20px; @@ -51,63 +74,59 @@ select{ .course-col:hover{ box-shadow: 0 0 20px 0px rgba(0,0,0,0.3); } -.go-back-section{ - padding-top: 5vh; - padding-left: 5vh; -} - - -/* ERROR HANDLE */ .error-message{ - color: #e53a1c; + color: var(--danger-color); padding: 10px; font-size: 16px; margin-bottom: 20px; } - - -/* BUTTONS */ .play-btn{ padding: 10px; text-decoration: none; - background-color: #242F40; + background-color: var(--safe-color); border-color: transparent; - color: #FFFFFF; + color: var(--text-light-color); border-radius: 10px; } .play-btn:hover { - background-color: #425575; + background-color: var(--safe-hover); transition: 0.4s ; } .play-btn:active{ - background-color: #425575; + background-color: var(--safe-active); transition: 0.2s ; } .edit-btn{ padding: 10px; text-decoration: none; - background-color: #CCA43B; + background-color: var(--option-color); border-color: transparent; color: #242F40; border-radius: 10px; cursor: pointer; } +.edit-btn:hover{ + background-color: var(--option-hover); +} +.edit-btn:active{ + background-color: var(--option-active); +} .delete-btn{ padding: 10px; - background-color: #cc513b; + background-color: var(--danger-color); border-color: transparent; - color: #242F40; + color: var(--text-dark-color); border-radius: 10px; font-family: monospace; cursor: pointer; text-decoration: none; } .delete-btn:hover{ - background-color: #d9563e; + background-color: var(--danger-hover); } .add-Btn { - background-color: #242F40; + background-color: var(--safe-color); color: white; font-size: 16px; padding: 10px; @@ -118,14 +137,14 @@ select{ } .add-Btn:hover{ cursor: pointer; - background-color: #2f3d54; + background-color: var(--safe-hover); } .add-Btn:active{ - background-color: #425575; + background-color: var(--safe-active); transition: 0.2s ; } .save-Btn{ - background-color: #CCA43B; + background-color: var(--option-color); font-size: 24px; padding: 15px; border-radius: 20px; @@ -133,9 +152,8 @@ select{ margin-top: 10px; cursor: pointer; } - .close-btn{ - background-color: #d2442b; + background-color: var(--danger-color); padding: 10px; border-radius: 5px; border-color: transparent; @@ -146,10 +164,10 @@ select{ color: white; } .close-btn:hover { - background-color: #e15238; + background-color: var(--danger-hover); } .submit-btn{ - background-color: #CCA43B; + background-color: var(--option-color); font-size: 20px; padding: 10px; border-radius: 5px; @@ -159,20 +177,14 @@ select{ cursor: pointer; width: 120px; height: 50px; - } .submit-btn:hover{ - background-color: #a67d0e; - color: white; + background-color: var(--option-hover); } .submit-btn:active{ - background-color: #2D333B; - color: white; + background-color: var(--option-active); } - - - @media (max-width: 700px){ .row{ flex-direction: column; diff --git a/FullstackProsjekt/src/frontend/src/components/TheWelcome.vue b/FullstackProsjekt/src/frontend/src/components/TheWelcome.vue index 9b3049bff693381e4263264d5c20c345b1ee4889..2a28a34e4a0921861842b8cdcc0afd8643559cfa 100644 --- a/FullstackProsjekt/src/frontend/src/components/TheWelcome.vue +++ b/FullstackProsjekt/src/frontend/src/components/TheWelcome.vue @@ -1,53 +1,43 @@ -<script> -import axios from "axios"; -export default { - name: 'Home', - async created() { - const response = await axios.get('user'); - console.log(response) - } -} -</script> - - <template> <body> + <!-- Header --> <section class ="header"> <div class="text-box"> <img id="logo" src="../components/icons/brain.png"/> <h1 class="heading">BrainStormer</h1> - <p> An easy way to learn and share quizzes. <br> Make your own quiz now! </p> + <p> Get ready to challenge your knowledge and have fun! <br> Experience a new way of learning </p> <router-link to="/dashboard" class="hero-btn">LOOK AT QUIZZES</router-link> </div> </section> - <!----- Info -----> + <!----- Info section -----> <section class="info"> - <h1>How does it work</h1> - <p>Set a difficulty to your quizzes ..... [Add more text here]</p> + <h1>Ace your classes with our new learning platform</h1> + <p>Explore our various difficulties and challenge yourself</p> <div class="row"> <div class="course-col"> <h3>Easy</h3> - <p>The simplest of quizzes </p> + <p>Suitable for beginners or those with basic knowledge on the topic.</p> </div> <div class="course-col"> <h3>Medium</h3> - <p>A more challenging quiz for those who want a challenge </p> + <p>Suitable for intermediate users with some experience on the topic.</p> </div> <div class="course-col"> <h3>Hard</h3> - <p>Quizzes that challenge the mind to new limits... </p> + <p>Suitable for advanced users or experts on the topic. </p> </div> </div> </section> <div class="space"> </div> <section class="cta"> - <h1> Enroll For Our Various Online Quizzes </h1> + <h1> Include and engage every student </h1> <router-link to="/about" class="hero-btn"> About us</router-link> </section> + <!----- Footer -----> <footer class="footer"> <div class="footer-content"> <p>© 2024 BrainStormer. All rights reserved.</p> @@ -62,25 +52,35 @@ export default { </template> +<script> +import axios from "axios"; +export default { + name: 'Home', + async created() { + const response = await axios.get('user'); + console.log(response) + } +} +</script> + <style scoped> .header{ min-height: 100vh; width: 100%; - background-image: linear-gradient(rgba(4,9,30,0.7), rgba(4,9,30,0.7)),url(photos/lightning.gif); + background-image: linear-gradient(rgba(4,9,30,0.7), rgba(4,9,30,0.7)),url(photos/mountain-backdrop.png); background-position: center; background-size: cover; position: relative; } #logo{ - height: 150px; - width: 150px; - padding: 10px; + height: 170px; + width: 170px; + padding: 5vh; } - .text-box{ width: 90%; - color: #fff; + color: var(--text-light-color); position: absolute; top: 50%; left: 50%; @@ -88,18 +88,17 @@ export default { text-align: center; } .text-box h1{ - font-size: 62px; + font-size: 60px; } .text-box p{ margin: 10px 0 40px; - font-size: 18px; - color: #fff; + font-size: 20px; + color: var(--text-light-color); } - .hero-btn{ display: inline-block; text-decoration: none; - color: #fff; + color: var(--text-light-color); border: 1px solid #fff; padding: 12px 34px; font-size: 16px; @@ -109,12 +108,10 @@ export default { } .hero-btn:hover{ border: 1px solid #CCA43B; - background: #CCA43B; + background: var(--option-hover); transition: 1s; color: #242F40; } - -/* Info section */ .info{ width: 80%; margin: auto; @@ -126,24 +123,21 @@ h1{ font-weight: 600; } p{ - color: #0f1412; + color: var(--text-dark-color); font-size: 16px; font-weight: 300; line-height: 22px; padding: 10px; } - h3{ text-align: center; font-weight: 600; margin: 10px 0; } - -/* CTA */ .cta{ margin: 100px auto; width: 80%; - background-image: linear-gradient(rgba(4,9,30,0.7), rgba(4,9,30,0.7)),url(photos/background.png); + background-image: linear-gradient(rgba(4,9,30,0.7), rgba(4,9,30,0.7)),url(photos/office-backdrop.png); background-position: center; background-size: cover; border-radius: 10px; @@ -151,14 +145,12 @@ h3{ padding: 100px 0; } .cta h1{ - color: #F2F2F2; + color: var(--text-light-color); margin-bottom: 40px; padding: 0; } - -/* Footer */ .footer { - background-color: #242F40; + background-color: var(--sidebar-bd-color); padding: 10px 0; text-align: center; } @@ -181,15 +173,12 @@ h3{ margin-right: 0; } .footer ul li a { - color: #fff; + color: var(--text-light-color); text-decoration: none; } .footer ul li a:hover { text-decoration: underline; } - - -/* Media for other devices */ @media (max-width: 700px){ .text-box h1{ font-size: 42px; @@ -199,5 +188,4 @@ h3{ font-size: 24px; } } - </style> diff --git a/FullstackProsjekt/src/frontend/src/components/icons/brain.png b/FullstackProsjekt/src/frontend/src/components/icons/brain.png index 586d86a67e5cac04950d9ccc38f645d395cc4d9d..2d6ef3db6be7205af8c797861f08ceb2fda98192 100644 Binary files a/FullstackProsjekt/src/frontend/src/components/icons/brain.png and b/FullstackProsjekt/src/frontend/src/components/icons/brain.png differ diff --git a/FullstackProsjekt/src/frontend/src/components/photos/background.png b/FullstackProsjekt/src/frontend/src/components/photos/background.png deleted file mode 100644 index e968b4bd6100bf6b3261beee0053e82708d0f964..0000000000000000000000000000000000000000 Binary files a/FullstackProsjekt/src/frontend/src/components/photos/background.png and /dev/null differ diff --git a/FullstackProsjekt/src/frontend/src/components/photos/developers/MadJon.png b/FullstackProsjekt/src/frontend/src/components/photos/developers/MadJon.png index 257608b39fc8906fc063f6e700d13ac803c8c383..b694016cba10fe4da94f9c27a614f56e8a6041c9 100644 Binary files a/FullstackProsjekt/src/frontend/src/components/photos/developers/MadJon.png and b/FullstackProsjekt/src/frontend/src/components/photos/developers/MadJon.png differ diff --git a/FullstackProsjekt/src/frontend/src/components/photos/lightning.gif b/FullstackProsjekt/src/frontend/src/components/photos/lightning.gif deleted file mode 100644 index df29d0b10f3df547f068b7aac9a42f16b2cf2b37..0000000000000000000000000000000000000000 Binary files a/FullstackProsjekt/src/frontend/src/components/photos/lightning.gif and /dev/null differ diff --git a/FullstackProsjekt/src/frontend/src/components/photos/mountain-backdrop.png b/FullstackProsjekt/src/frontend/src/components/photos/mountain-backdrop.png new file mode 100644 index 0000000000000000000000000000000000000000..5945b761b707f0823c9492adc11afe65727ad3ec Binary files /dev/null and b/FullstackProsjekt/src/frontend/src/components/photos/mountain-backdrop.png differ diff --git a/FullstackProsjekt/src/frontend/src/components/photos/office-backdrop.png b/FullstackProsjekt/src/frontend/src/components/photos/office-backdrop.png new file mode 100644 index 0000000000000000000000000000000000000000..c6dcad7739294e331b83e9e41bfe95046bd5a9f5 Binary files /dev/null and b/FullstackProsjekt/src/frontend/src/components/photos/office-backdrop.png differ diff --git a/FullstackProsjekt/src/frontend/src/components/photos/wheel-backdrop.png b/FullstackProsjekt/src/frontend/src/components/photos/wheel-backdrop.png new file mode 100644 index 0000000000000000000000000000000000000000..fe40f6ee980cef102f41bae1d7491e80731af859 Binary files /dev/null and b/FullstackProsjekt/src/frontend/src/components/photos/wheel-backdrop.png differ diff --git a/FullstackProsjekt/src/frontend/src/components/shared/EditQuestionModel.vue b/FullstackProsjekt/src/frontend/src/components/shared/EditQuestionModel.vue index e3ba789bf08840896d5ccf6f7aabcfe14b3389b5..268da91fe0b5a76ae94c8642f480496856104c53 100644 --- a/FullstackProsjekt/src/frontend/src/components/shared/EditQuestionModel.vue +++ b/FullstackProsjekt/src/frontend/src/components/shared/EditQuestionModel.vue @@ -86,6 +86,7 @@ export default { <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"> diff --git a/FullstackProsjekt/src/frontend/src/components/shared/PlayQuiz/DisplayQuestion.vue b/FullstackProsjekt/src/frontend/src/components/shared/PlayQuiz/DisplayQuestion.vue new file mode 100644 index 0000000000000000000000000000000000000000..4ccca83788fc593887d3decbc9a4fb049937ad5d --- /dev/null +++ b/FullstackProsjekt/src/frontend/src/components/shared/PlayQuiz/DisplayQuestion.vue @@ -0,0 +1,209 @@ +<script> +import {apiClient} from "@/api.js"; + +export default { + props: { + question: Object, + selectedOption: String, + }, + data() { + return { + questionText: null, + options: [], + correctAnswer: null, + } + }, + mounted() { + //this.getSampleQuestion(); + }, + methods: { + getSampleQuestion() { + this.question = { + id: 3, + questionText: "question 3", + options: ["ans1","ans2"], + answer: "ans2" + } + }, + getQuestion(questionId) { + try { + apiClient.get('/questions/question/' + this.questionId).then(response => { + this.question = JSON.parse(response.data); + this.questionText = response.data.questionText; + this.options = JSON.parse(response.data.options); + this.correctAnswer = response.data.answer; + }); + } catch (error) { + //TODO: proper error handling + this.errorMsg = 'Error retrieving question'; + } + }, + selectOption(option) { + this.$emit('update:selectedOption', option); + } + } +} +</script> + +<template> + <div class="quiz"> + + <div id="info"> + <div id="score">Score: 0</div> + <div id="ques-left">Question:1/20</div> + </div> + <div id="ques-view"> + + </div> + <div class="question"> + <h1>Q title</h1> + </div> + + <ul v-if="question.options"> + <li v-for="(option) in question.options" :key="option"> + <!-- + <input type="radio" :id="'option'" :value="option" + :checked="option === selectedOption" @change="selectOption(option)"> + --> + + <label :for="option">option label</label> + </li> + </ul> + <!-- + <div class="choice"> + <div class="options"><input type="radio" name="options" value="option1" id="opt0"><label for="opt0" id="lb0">Option1</label></div> + <div class="options"><input type="radio" name="options" value="option2" id="opt1"><label for="opt1" id="lb1">Option2</label></div> + <div class="options"><input type="radio" name="options" value="option3" checked="checked" id="opt2"><label for="opt2" id="lb2">Option3</label></div> + <div class="options"><input type="radio" name="options" value="option4" id="opt3"><label for="opt3" id="lb3">Option4</label></div> + </div> + + <div class="ans-btn"> + <button type="button" class="submit-answer">Submit Answer</button> + <a href="#display-final-score" type="button" class="view-results">View Results</a> + </div> + --> + + </div> + + +</template> + +<style scoped> +.quiz{ + text-align: center; + margin-top: 20px; + height: 100vh; +} + +#info{ + height:25px; +} + +#score{ + width:50%; + float:left; + font-size: 25px; +} + +#ques-left{ + width:50%; + float:left; + font-size:25px; +} + +#ques-view{ + height: 35px; + margin-top: 10px; + padding: 2px; +} +.question{ + letter-spacing: .13em; +} + +.choice{ + padding: 3%; +} + +.options{ + display:block; + font-size: 25px; + margin-top: 30px; + text-align: left; +} + +input[type=radio] { + border: 5px solid white; + width: 20px; + height: 1.3em; + float: left; +} + +.ans-btn{ + padding: 2%; +} + +.submit-answer{ + border: 2px solid #CCA43B; + padding: 15px; + border-radius: 20px; + transition: background-color 0.3s,border 0.2s, color 0.2s; + margin-right: 10px; + font-family: monospace; +} + +.submit-answer:hover{ + background-color: #CCA43B; + padding: 16px; + color: #fff; + cursor: pointer; + border-radius: 20px; +} + +.view-results{ + text-decoration: none; + color: black; + border: 2px solid #CCA43B; + padding: 15px; + border-radius: 20px; + transition: background-color 0.3s,border 0.2s, color 0.2s; +} + + +.view-results:hover{ + background-color: #CCA43B; + padding: 16px; + color: #fff; + cursor: pointer; + border-radius: 20px; +} + + +@media only screen and (max-width: 1250px) { + .options{ + margin-left: 8% + } +} + +@media only screen and (max-width: 850px) { + .options{ + margin-left: 4%; + } +} + +@media only screen and (max-width: 650px) { + .options{ + display: block; + margin-top: 3%; + + } +} + +@media only screen and (max-width: 550px) { + .options{ + margin-top: 3%; + + } + +} + +</style> \ No newline at end of file diff --git a/FullstackProsjekt/src/frontend/src/components/shared/PlayQuiz/QuizResult.vue b/FullstackProsjekt/src/frontend/src/components/shared/PlayQuiz/QuizResult.vue new file mode 100644 index 0000000000000000000000000000000000000000..48ad0dbfa0f3544a14cfe643142368254454fdd1 --- /dev/null +++ b/FullstackProsjekt/src/frontend/src/components/shared/PlayQuiz/QuizResult.vue @@ -0,0 +1,87 @@ +<script> +export default { + name: "QuizResult" +} +</script> + +<template> + <div class="final-result"> + <h1>The Quiz is Over</h1> + <div class="solved-ques-no">You Solved 10 questions of {Name of quiz here}</div> + <div class="right-wrong">3/4 were right</div> + <div id="display-final-score">Your Final Score is: 35</div> + <div class="remark">Remark: Satisfactory, Keep trying!</div> + <button id="restart">Restart Quiz</button> + </div> +</template> + +<style scoped> + +/*Final Results*/ +.final-result{ + text-align: center; + padding: 10px; + font-size: 1.5em; + height: 100vh; +} +.solved-ques-no{ + padding: 10px; +} + +.right-wrong{ + padding: 10px; +} + +#display-final-score{ + padding: 5%; +} +.remark{ + padding: 5% +} +#restart{ + background-color:#CCA43B; + margin-left: 30px; + border: 2px solid #CCA43B; + padding: 15px; + border-radius: 20px; + font-size: 80%; + transition: background-color 0.3s,border 0.2s, color 0.2s; +} + +#restart:hover{ + background-color: #CCA43B; + color:#fff; + cursor: pointer; + /* width: 120px;*/ + font-size: 90%; +} + + +@media only screen and (max-width: 1250px) { + .options{ + margin-left: 8% + } +} + +@media only screen and (max-width: 850px) { + .options{ + margin-left: 4%; + } +} + +@media only screen and (max-width: 650px) { + .options{ + display: block; + margin-top: 3%; + + } +} + +@media only screen and (max-width: 550px) { + .options{ + margin-top: 3%; + + } + +} +</style> \ No newline at end of file diff --git a/FullstackProsjekt/src/frontend/src/components/shared/create-quiz/CreateQuizView.vue b/FullstackProsjekt/src/frontend/src/components/shared/create-quiz/CreateQuizView.vue index e6512e2c9c8f1ea37d9cfe60cc7a294f2eb69278..96b5eb784e6c080124447915ce3caefe0199b197 100644 --- a/FullstackProsjekt/src/frontend/src/components/shared/create-quiz/CreateQuizView.vue +++ b/FullstackProsjekt/src/frontend/src/components/shared/create-quiz/CreateQuizView.vue @@ -6,7 +6,6 @@ import {categoryEnums} from "@/data/categories.js" import {difficultyEnums} from "@/data/difficulties.js"; import Svg from "@/assets/Svg.vue"; -//like editquiz, but w/o questions, redirect to edit when quiz is constructed export default { components: {Svg}, @@ -24,7 +23,6 @@ export default { categories: categoryEnums, selectedDifficulty: null, difficulties: difficultyEnums, - //TODO: make quiz object }; }, mounted() { @@ -57,7 +55,6 @@ export default { <template> <body> <router-link to="/overviewQuiz" ><Svg name="go-back-icon" class="go-back-section"/></router-link> - <div class="new-quiz-page"> <form @submit.prevent="constructQuiz"> <div class="newQuizDiv"> @@ -103,6 +100,11 @@ export default { padding: 0 20px; } +.go-back-section{ + padding-top: 5vh; + padding-left: 5vh; +} + input, select { width: 100%; diff --git a/FullstackProsjekt/src/frontend/src/components/shared/modal/Modal.vue b/FullstackProsjekt/src/frontend/src/components/shared/modal/Modal.vue index 7a9eacb5880165e0856ac74fe1da5af907aa00b7..77a8b95241e966b7ec845fe9f57e0b6064d7ba65 100644 --- a/FullstackProsjekt/src/frontend/src/components/shared/modal/Modal.vue +++ b/FullstackProsjekt/src/frontend/src/components/shared/modal/Modal.vue @@ -48,32 +48,26 @@ const props = defineProps({ margin: auto; padding: 20px 30px; background-color: #fff; - border-radius: 2px; + border-radius: 10px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33); transition: all 0.3s ease; } - .modal-header h3 { margin-top: 0; - color: #42b983; + color: #242f40; } - .modal-body { margin: 20px 0; } - .modal-default-button { float: right; } - .modal-enter-from { opacity: 0; } - .modal-leave-to { opacity: 0; } - .modal-enter-from .modal-container, .modal-leave-to .modal-container { -webkit-transform: scale(1.1); diff --git a/FullstackProsjekt/src/frontend/src/components/shared/sidebar/Sidebar.vue b/FullstackProsjekt/src/frontend/src/components/shared/sidebar/Sidebar.vue index d0014adffe8bd3f2db9a8b89d0b5926273770e25..df220195220e063ba15d59ccbc22eaa594e4281a 100644 --- a/FullstackProsjekt/src/frontend/src/components/shared/sidebar/Sidebar.vue +++ b/FullstackProsjekt/src/frontend/src/components/shared/sidebar/Sidebar.vue @@ -1,62 +1,56 @@ +<template> + <div class="sidebar" :style="{width: sidebarWidth}"> + + <SidebarLink to="/" icon="home-icon">Home</SidebarLink> + <SidebarLink to="/dashboard" icon="dashboard-icon">Dashboard</SidebarLink> + <SidebarLink to="/about" icon="about-us-icon">About</SidebarLink> + <SidebarLink to="/feedback" icon="feedback-icon">Feedback</SidebarLink> + <SidebarLink to="/login" icon="login-icon">Login</SidebarLink> + <SidebarLink v-if="isLoggedIn" to="/profile" icon="profile-icon">Profile</SidebarLink> + + <span class="collapse-icon" :class="{'rotate-180': collapsed}" @click="toggleSideBar"> + <Svg name="double-arrow" class="sidebar-c-icon"/> + </span> + + </div> +</template> + + <script> import { collapsed, toggleSideBar, sidebarWidth } from "@/components/shared/sidebar/state.js"; import SidebarLink from "@/components/shared/sidebar/SidebarLink.vue"; import Svg from "@/assets/Svg.vue"; +import {ref} from "vue"; export default { components: {Svg, SidebarLink}, props: {}, setup() { + const isLoggedIn = ref(false); + const handleClickOutside = (event) => { const sidebar = document.querySelector('.sidebar'); if (sidebar && !sidebar.contains(event.target)) { collapsed.value = true; // Collapse the sidebar } } - document.addEventListener('click', handleClickOutside); - const beforeUnmount = () => { document.removeEventListener('click', handleClickOutside); }; - - return { collapsed, toggleSideBar, sidebarWidth, beforeUnmount }; + return { collapsed, toggleSideBar, sidebarWidth, beforeUnmount, isLoggedIn }; } } </script> -<template> - <div class="sidebar" :style="{width: sidebarWidth}"> - - <SidebarLink to="/" icon="home-icon">Home</SidebarLink> - <SidebarLink to="/dashboard" icon="dashboard-icon">Dashboard</SidebarLink> - <SidebarLink to="/about" icon="about-us-icon">About</SidebarLink> - <SidebarLink to="/feedback" icon="feedback-icon">Feedback</SidebarLink> - <SidebarLink to="/login" icon="login-icon">Login</SidebarLink> - <SidebarLink to="/profile" icon="profile-icon">Profile</SidebarLink> - - <span class="collapse-icon" :class="{'rotate-180': collapsed}" @click="toggleSideBar"> - <Svg name="double-arrow" class="sidebar-c-icon"/> - </span> - - </div> -</template> - - - <style> -:root{ - --sidebar-bd-color: #242F40; - --sidebar-item-hover: #CCA43B; - --sidebar-item-active: #CCA43B; -} + </style> <style scoped> .sidebar{ - color: white; + color: var(--text-light-color); background-color: var(--sidebar-bd-color); - float: left; position: fixed; z-index: 1; @@ -64,14 +58,11 @@ export default { left: 0; bottom: 0; padding: 0.5rem; - transition: 0.25s ease; - display: flex; flex-direction: column; margin-left: -38px; } - .collapse-icon{ position: absolute; bottom: 0; @@ -82,7 +73,6 @@ export default { height: 30px; width: 30px; } - .rotate-180{ transform: rotate(180deg); transition: 0.2s linear; diff --git a/FullstackProsjekt/src/frontend/src/components/shared/sidebar/SidebarLink.vue b/FullstackProsjekt/src/frontend/src/components/shared/sidebar/SidebarLink.vue index bb2a30fabd77709f336592c6f830e9ff5d046dbc..4d6b8556a7d723e7087e207fd710813dfcf340bd 100644 --- a/FullstackProsjekt/src/frontend/src/components/shared/sidebar/SidebarLink.vue +++ b/FullstackProsjekt/src/frontend/src/components/shared/sidebar/SidebarLink.vue @@ -1,3 +1,17 @@ +<template> + <router-link :to="to" class="link" :class="{ active: isActive }"> + <div class="icon-wrapper" v-if="collapsed"> + <Svg :name="icon" class="icon" /> + </div> + <Transition name="fade"> + <span v-if="!collapsed" class="link-content"> + <Svg :name="icon" class="icon" /> + <slot /> + </span> + </Transition> + </router-link> +</template> + <script> import { computed } from 'vue' import { useRoute } from 'vue-router' @@ -18,32 +32,16 @@ export default { } </script> -<template> - <router-link :to="to" class="link" :class="{ active: isActive }"> - <div class="icon-wrapper" v-if="collapsed"> - <Svg :name="icon" class="icon" /> - </div> - <Transition name="fade"> - <span v-if="!collapsed" class="link-content"> - <Svg :name="icon" class="icon" /> - <slot /> - </span> - </Transition> - </router-link> -</template> - <style scoped> .fade-enter-active, .fade-leave-active { transition: opacity 0.1ms ease; } - .fade-enter, .fade-leave-to { transform: translateX(20px); opacity: 0; } - .link { font-weight: 400; padding: 0.2em; @@ -53,22 +51,18 @@ export default { text-decoration: none; margin: 1px; } - .link:hover { background-color: var(--sidebar-item-hover); } - .link.active { background-color: var(--sidebar-item-active); color: #242f40; } - .icon { width: 2.2em; height: 2.2em; margin-right: 1vh; } - .link-content { display: flex; align-items: center; diff --git a/FullstackProsjekt/src/frontend/src/components/shared/sidebar/state.js b/FullstackProsjekt/src/frontend/src/components/shared/sidebar/state.js index 8c0f7d43c12dc16a7d6b12a9bc855c1fa2bed5cd..1a415a27ffe8f707a6e836ee416cee6262011c2c 100644 --- a/FullstackProsjekt/src/frontend/src/components/shared/sidebar/state.js +++ b/FullstackProsjekt/src/frontend/src/components/shared/sidebar/state.js @@ -1,9 +1,7 @@ - import {ref, computed} from 'vue' export const collapsed = ref(true) export const toggleSideBar = () => (collapsed.value = !collapsed.value) - export const SIDEBAR_WIDTH = 180 export const SIDEBAR_WIDTH_COLLAPSED = 80 //38 export const sidebarWidth = computed( diff --git a/FullstackProsjekt/src/frontend/src/models/quiz.js b/FullstackProsjekt/src/frontend/src/models/quiz.js index 8099a99587f29816a631ee5be2a5c52586cb9ce7..d83893d032790a3c7fb85b621ba26b68ebde3566 100644 --- a/FullstackProsjekt/src/frontend/src/models/quiz.js +++ b/FullstackProsjekt/src/frontend/src/models/quiz.js @@ -1,16 +1,12 @@ export default class Quiz { - constructor(quizId, title, creatorId, questions, category, difficulty) { + constructor(quizId, title, creatorId, category, difficulty) { this.quizId = quizId; this.title = title; this.creatorId = creatorId; - this.questions = questions; this.category = category; this.difficulty = difficulty; } - - - } /** diff --git a/FullstackProsjekt/src/frontend/src/router/index.js b/FullstackProsjekt/src/frontend/src/router/index.js index 97567eafea7dcfd2b37885eea3873ac727f902c0..2b39d738e96089e3f5f5576e1161010ff8207200 100644 --- a/FullstackProsjekt/src/frontend/src/router/index.js +++ b/FullstackProsjekt/src/frontend/src/router/index.js @@ -1,6 +1,8 @@ import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' import EditQuizView from "@/components/shared/create-quiz/EditQuizView.vue"; +import PlayQuizView from "@/views/PlayQuizView.vue"; +import CreateQuizView from "@/components/shared/create-quiz/CreateQuizView.vue"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -35,20 +37,20 @@ const router = createRouter({ name: 'signup', component: () => import('../views/SignupView.vue') }, + { + path: '/createQuiz', + name: 'create Quiz', + component: CreateQuizView + }, { path: '/overviewQuiz', name: 'overview Quiz', component: () => import('../views/OverviewQuizView.vue') }, - { - path: '/create-quiz', - name: 'create Quiz', - component: () => import('../components/shared/create-quiz/CreateQuizView.vue') - }, { path: '/play-quiz/:quizId', name: 'playQuiz', - component: () => import('../views/PlayQuizView.vue'), + component: PlayQuizView, params: true }, { @@ -61,7 +63,7 @@ const router = createRouter({ path: '/profile', name: 'profile', component: () => import('../views/ProfileView.vue') - }, + } ] }) diff --git a/FullstackProsjekt/src/frontend/src/views/AboutView.vue b/FullstackProsjekt/src/frontend/src/views/AboutView.vue index 909d494df9e140d992e835bf0e304dedcffedf29..2bdbaf6e7131c451c9aff2be4af512bd362987cf 100644 --- a/FullstackProsjekt/src/frontend/src/views/AboutView.vue +++ b/FullstackProsjekt/src/frontend/src/views/AboutView.vue @@ -4,30 +4,29 @@ <h1>About</h1> <p>Welcome to our quiz web application! We provide a comprehensive and user-friendly platform for creating, managing, and taking quizzes for educational, training, or entertainment purposes.</p> <div class="space"> </div> - <h2>Our Features</h2> + </div> + + <div> <div class="columns"> + <h2>Our Features</h2> + <div class="column"> <ul> - <li>Quiz Creation: Create quizzes with various question types, difficulty levels, and multimedia elements.</li> - <li>Tagging and Categorization: Organize your questions with difficulty level and categories.</li> - <li>Search and Filter: Quickly find relevant questions based on categories and difficulty levels.</li> - <li>Question Management: Add, edit, delete, and organize questions within quizzes.</li> - </ul> - </div> - <div class="column"> - <ul> - <li>Quiz Templates: Use pre-designed or customizable templates for easy quiz creation.</li> - <li>Import and Export: Import questions from external sources and export quizzes in various formats.</li> - <li>Scoring and Feedback: Automatically score quizzes and provide immediate feedback.</li> - <li>Progress Tracking: Track your progress, view past quiz attempts, and monitor performance statistics.</li> - <li>Feedback and Support: Provide feedback, report issues, and access customer support.</li> + <li><strong>Quiz Creation:</strong> Create quizzes with various question types, difficulty levels, and multimedia elements.</li> + <li><strong>Tagging and Categorization:</strong> Organize your questions with difficulty level and categories.</li> + <li><strong>Search and Filter:</strong> Quickly find relevant questions based on categories and difficulty levels.</li> + <li><strong>Question Management:</strong> Add, edit, delete, and organize questions within quizzes.</li> + <li><strong>Quiz Templates:</strong> Use pre-designed or customizable templates for easy quiz creation.</li> + <li><strong>Import and Export:</strong> Import questions from external sources and export quizzes in various formats.</li> + <li><strong>Scoring and Feedback:</strong> Automatically score quizzes and provide immediate feedback.</li> + <li><strong>Progress Tracking:</strong> Track your progress, view past quiz attempts, and monitor performance statistics.</li> + <li><strong>Feedback and Support:</strong> Provide feedback, report issues, and access customer support.</li> </ul> </div> </div> </div> - <h1 class ="our-team" style="text-align:center">Our Team</h1> <div class="row"> <div class="column"> @@ -38,7 +37,6 @@ <p class="title">Developer</p> <p>2 year Bachelor in Computer Science at NTNU Trondheim </p> <p>torbjorn@ntnu.no</p> - <p><button class="button">Contact</button></p> </div> </div> </div> @@ -51,7 +49,6 @@ <p class="title">Developer</p> <p>2 year Bachelor in Computer Science at NTNU Trondheim</p> <p>heine@ntnu.no</p> - <p><button class="button">Contact</button></p> </div> </div> </div> @@ -65,7 +62,6 @@ <p class="title">Developer</p> <p>2 year Bachelor in Computer Science at NTNU Trondheim</p> <p>kristiane@ntnu.no</p> - <p><button class="button">Contact</button></p> </div> </div> </div> @@ -78,7 +74,6 @@ <p class="title">Developer</p> <p>2 year Bachelor in Computer Science at NTNU Trondheim</p> <p>madelesj@ntnu.no</p> - <p><button class="button">Contact</button></p> </div> </div> </div> @@ -87,23 +82,31 @@ </body> </template> + <script> export default { name: 'About', }; </script> + <style> +.about-section { + background-image: linear-gradient(rgba(4,9,30,0.7), rgba(4,9,30,0.7)),url("@/components/photos/wheel-backdrop.png"); + padding: 20px; +} .columns { display: flex; justify-content: space-between; + margin: 10vh; +} +.column ul li { + margin-bottom: 20px; } - .column { flex: 1; margin-right: 20px; } - .column p{ font-size: 14px; } @@ -113,48 +116,27 @@ p { li{ font-size: 16px; } - .card { box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); - margin: 8px; + margin: 8px auto; + padding: 10px; + max-width: 400px; } - .about-section { padding: 10vh 10vh 10vh 10vh; text-align: center; - background-color: #858A93; - color: #242F40; + color: var(--text-light-color); } .our-team{ - color: #242F40; + color: var(--sidebar-bd-color); } - .container { padding: 0 16px; - color: #242F40; + color: var(--sidebar-bd-color); } - .title { color: grey; } - -.button { - border: none; - outline: 0; - display: inline-block; - padding: 8px; - color: white; - background-color: #363636; - text-align: center; - cursor: pointer; - width: 100%; - font-family: monospace; -} - -.button:hover { - background-color: #555; -} - #logo{ width: 100%; } @@ -163,6 +145,7 @@ li{ .column { width: 100%; display: block; + max-width: 100%; } } </style> diff --git a/FullstackProsjekt/src/frontend/src/views/DashboardView.vue b/FullstackProsjekt/src/frontend/src/views/DashboardView.vue index 82e4701518389e7a65c80c33bbb59ea567dc8014..b383a4e48fc83b01fec84170ffa0a7e02b607043 100644 --- a/FullstackProsjekt/src/frontend/src/views/DashboardView.vue +++ b/FullstackProsjekt/src/frontend/src/views/DashboardView.vue @@ -1,89 +1,77 @@ -<script > - import { apiClient } from "@/api.js"; - import router from "@/router/index.js"; - import Svg from "@/assets/Svg.vue"; - import { categoryEnums } from "@/data/categories.js"; - - export default { - components: { Svg }, - props: { - quizId: { - type: Number, - required: true, -}, -}, - data() { - return { - quizList: [] - }; -}, - mounted() { - this.getQuiz(); -}, - methods: { - async getQuiz() { - try { - const response = await apiClient.get('/quiz/'); - this.quizList = response.data; - - console.log(this.quizList[0]) - } catch (error) { - // TODO: Proper error handling - console.error('Error retrieving quiz:', error); - } -}, - getIcon(category) { - // Check if the category exists in the enum - if (categoryEnums.includes(category)) { - // Retrieve the icon name from the mapping - return categoryIcons[category] || categoryIcons.Default; -} else { - // If category not found, return the default icon - return categoryIcons.Default; -} -}, - playQuiz(id) { - console.log(id) - router.push({ name: 'playQuiz', params: { quizId: id } }); -}, -}, -}; -</script> - - <template> - <body class="dashboard"> - <div class="top-bar"> + <body> + <div class="dashboard"> + <div class="top-bar"> + <router-link to="/" ><Svg name="go-back-icon" class="go-back-icon"/></router-link> + + <div class="search-container"> + <input class="searchBox" v-model="searchTerm" placeholder="Search for category..."> + </div> - <div class="search-container"> - <input class="searchBox" placeholder="Search for category..."> - </div> <br> - <div class="create-container"> - <router-link to="/overviewQuiz" class="create-btn">YOUR QUIZES</router-link> + <div class="create-container"> + <router-link to="/overviewQuiz" class="add-Btn">YOUR QUIZZES</router-link> + </div> </div> - </div> <div class="row"> - <div class="quiz-list"> - <div class="quiz-col" v-for="quiz in quizList" :key="quiz.id"> - <div class="quiz-header"> - <h3>{{ quiz.title }}</h3> - </div> - <div class="quiz-body"> - <p>Difficulty level: {{ quiz.difficulty }}</p> - <p>Category: {{ quiz.category }}</p> - </div> - <div class="quiz-footer"> - <button @click="playQuiz(quiz.id)" class="play-btn">Play</button> - </div> - </div> - </div> + <div class="quiz-list"> + <div class="quiz-col" v-for="quiz in filteredQuizList" :key="quiz.id"> + <div class="quiz-header"> + <h3>{{ quiz.title }}</h3> + </div> + <div class="quiz-body"> + <p>Difficulty level: {{ quiz.difficulty }}</p> + <p>Category: {{ quiz.category }}</p> + </div> + <div class="quiz-footer"> + <button @click="playQuiz(quiz.id)" class="play-btn">Play</button> + </div> + </div> + </div> </div> - + </div> </body> </template> +<script> +import { apiClient } from "@/api.js"; +import router from "@/router/index.js"; +import Svg from "@/assets/Svg.vue"; + +export default { + components: {Svg}, + data() { + return { + quizList: [], + searchTerm: '' + }; + }, + mounted() { + this.getQuiz(); + }, + methods: { + async getQuiz() { + try { + const response = await apiClient.get('/quiz/'); + this.quizList = response.data; + } catch (error) { + console.error('Error retrieving quiz:', error); + } + }, + playQuiz(id) { + router.push({ name: 'playQuiz', params: { quizId: id } }); + } + }, + computed: { + filteredQuizList() { + return this.quizList.filter(quiz => quiz.category.toLowerCase().includes(this.searchTerm.toLowerCase())); + } + } +}; +</script> + + <style> .dashboard{ padding: 20px; @@ -92,47 +80,32 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 20px; + margin-bottom: 10vh; + margin-top: 4vh; +} +.go-back-icon{ + margin-left: 1px; } .search-container { - flex-grow: 1; /* Grow to take available space */ - margin-right: 10px; /* Adjust margin between search box and button */ + flex-grow: 1; } - .create-container { - flex-shrink: 0; /* Do not shrink */ + flex-shrink: 0; } -.searchBox{ +.searchBox { width: 250px; padding: 10px; - margin: 0 auto; /* Center horizontally */ + margin: 0 auto; display: block; text-align: center; } - -.quiz-col{ +.quiz-col { flex-basis: 31%; background: #d7d7d7; border-radius: 10px; margin-bottom: 5%; padding: 20px; box-sizing: border-box; - transition: 0.5s;} - -.create-btn{ - text-decoration: none; - color: #E5E5E5; - padding: 12px 34px; - font-size: 16px; - cursor: pointer; - margin-bottom: 60px; - background-color: #242F40; -} -.create-btn:hover{ - border: 1px solid #CCA43B; - color: #242F40; - background: #CCA43B; - transition: 1s; + transition: 0.5s; } - </style> \ No newline at end of file diff --git a/FullstackProsjekt/src/frontend/src/views/FeedbackView.vue b/FullstackProsjekt/src/frontend/src/views/FeedbackView.vue index 5e3935d81fbe65909856f6499db0321194e77c8d..6772184191eb218edd1ee13de1dcabe20d577375 100644 --- a/FullstackProsjekt/src/frontend/src/views/FeedbackView.vue +++ b/FullstackProsjekt/src/frontend/src/views/FeedbackView.vue @@ -1,3 +1,36 @@ +<template> + <body> + <div class="feedback-page"> + <form @submit.prevent="handleSubmit"> + <div class="feedback"> + <h1>Feedback</h1> + <p>Is there anything you would like to tell us? Comment below!</p> + <div class="contactBox"> + <label>Name</label> <br> + <input class="feedback-input" type="text" required v-model="name" ref="nameInput"/> <br> + <label>Email</label> <br> + <input class="feedback-input" type="email" required v-model="email" ref="emailInput"/> + + <br> + <br> + <br> + <br> + + <label>Title</label> <br> + <input class="feedback-input" type="text" required v-model="title" ref="title"/> <br> + <textarea class="comment" required v-model="comment" placeholder="Tell us whats on your mind!" ref="commentInput"></textarea> + </div> + </div> + + <div class="submit-section"> + <input class="submit-btn" type="submit"/> + </div> + + </form> + </div> + </body> +</template> + <script> import Svg from "@/assets/Svg.vue"; @@ -41,7 +74,6 @@ export default { } }, mounted() { - // Restore form data from local storage const formData = JSON.parse(localStorage.getItem('feedbackForm')); if (formData) { this.name = formData.name; @@ -54,40 +86,6 @@ export default { </script> -<template> - <body> - <div class="feedback-page"> - <form @submit.prevent="handleSubmit"> - <div class="feedback"> - <h1>Feedback</h1> - <p>Is there anything you would like to tell us? Comment below!</p> - <div class="contactBox"> - <label>Name</label> <br> - <input class="feedback-input" type="text" required v-model="name" ref="nameInput"/> <br> - <label>Email</label> <br> - <input class="feedback-input" type="email" required v-model="email" ref="emailInput"/> - - <br> - <br> - <br> - <br> - - <label>Title</label> <br> - <input class="feedback-input" type="text" required v-model="title" ref="title"/> <br> - <textarea class="comment" required v-model="comment" placeholder="Tell us whats on your mind!" ref="commentInput"></textarea> - </div> - </div> - - <div class="submit-section"> - <input class="submit-btn" type="submit"/> - </div> - - </form> - </div> - </body> -</template> - - <style> .feedback-page{ padding: 10vh; @@ -95,32 +93,28 @@ export default { justify-content: center; align-items: center; } - .feedback { text-align: center; - color: #242F40; + color: var(--text-dark-color); border-color: transparent; border-radius: 15px; align-items: center; border-style: solid; } - .contactBox { padding: 30px; } .feedback-input{ width: 50vh; } - .comment { padding: 8px; border-radius: 5px; border: none; width: 50vh; height: 15vh; - background-color: #E5E5E5; + background-color: var(--field-background); } - .submit-section { display: flex; justify-content: center; @@ -130,8 +124,7 @@ export default { @media (max-width: 700px){ .feedback-input, .comment { - width: 90%; /* Adjust the width to fit smaller screens */ + width: 90%; } } - </style> diff --git a/FullstackProsjekt/src/frontend/src/views/LoginView.vue b/FullstackProsjekt/src/frontend/src/views/LoginView.vue index 58971f1703ea4e835873536c823faef9cf2786d9..649cf886614c14c9f9e9c8ebe1d576f9c00372e4 100644 --- a/FullstackProsjekt/src/frontend/src/views/LoginView.vue +++ b/FullstackProsjekt/src/frontend/src/views/LoginView.vue @@ -1,44 +1,3 @@ -<script> -import Svg from "@/assets/Svg.vue"; -import {setToken} from "@/tokenController.js"; -import {apiClient} from "@/api.js"; - -export default { - name: 'Login', - components: {Svg}, - data() { - return { - username: '', - email: '', - password: '', - showPassword: false, - errorMsg: '', - } - }, - methods: { - async handleSubmit() { - try { - await apiClient.post('/auth/login', { - username: this.username, - password: this.password - }).then(response => { - alert(this.username + " is now logged in!") - setToken(response.data.jwt); //TODO: check token name - localStorage.setItem('username', this.username); - this.$router.push('/profile'); - }); - } catch (error) { - //TODO: proper error handling - this.errorMsg = 'Error logging in, try again'; - } - }, - togglePasswordVisibility() { - this.showPassword = !this.showPassword; - } - } -} -</script> - <template> <body> <div class="loginPage"> @@ -49,7 +8,7 @@ export default { <div class="loginBox"> <label>Username</label> <br> - <input type="text" required v-model="username" placeholder="Rizz_Dragon420"/> <br> + <input type="text" required v-model="username" placeholder="PartyDragon42"/> <br> <label>Password</label> <br> <div class="password-input"> @@ -73,6 +32,49 @@ export default { </body> </template> + +<script> +import Svg from "@/assets/Svg.vue"; +import {setToken} from "@/tokenController.js"; +import {apiClient} from "@/api.js"; + +export default { + name: 'Login', + components: {Svg}, + data() { + return { + username: '', + email: '', + password: '', + showPassword: false, + errorMsg: '', + } + }, + methods: { + async handleSubmit() { + try { + await apiClient.post('/auth/login', { + username: this.username, + password: this.password + }).then(response => { + alert(this.username + " is now logged in!") + setToken(response.data.jwt); //TODO: check token name + localStorage.setItem('username', this.username); + this.$router.push('/profile'); + }); + } catch (error) { + //TODO: proper error handling + this.errorMsg = 'Error logging in, try again'; + } + }, + togglePasswordVisibility() { + this.showPassword = !this.showPassword; + } + } +} +</script> + + <style> .loginPage { padding-top: 85px; @@ -80,7 +82,6 @@ export default { justify-content: center; align-items: center; } - .login { text-align: center; padding: 20px; @@ -90,18 +91,14 @@ export default { margin: 20px; max-width: 450px; } - .loginBox { padding: 40px; } - .password-input { position: relative; display: inline-block; min-width: 300px; } - - .showPasswordIcon { position: absolute; top: 50%; @@ -112,16 +109,15 @@ export default { cursor: pointer; } #signUpLink { - color: #CCA43B; + color: var(--option-color); padding: 10px; font-size: 16px; text-decoration: none; } #signUpLink:hover{ - color: #242F40; + color: var(--option-hover); transition: 0.3s; } - .submit-section { display: flex; justify-content: center; @@ -129,8 +125,12 @@ export default { } @media (max-width: 700px) { - - + .login{ + max-width: 300px; + } + .password-input{ + min-width: 200px; + } } </style> diff --git a/FullstackProsjekt/src/frontend/src/views/NewQuizView.vue b/FullstackProsjekt/src/frontend/src/views/NewQuizView.vue new file mode 100644 index 0000000000000000000000000000000000000000..e4c73e449142e90769b2bc76ee74a32a3e149faa --- /dev/null +++ b/FullstackProsjekt/src/frontend/src/views/NewQuizView.vue @@ -0,0 +1,120 @@ +<script> +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> + <div class="new-quiz-page"> + <form @submit.prevent="constructQuiz"> + <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">{{categories.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">{{difficulties.difficulty}}</option> + </select> + </form> + </div> + + <div class="footer"> + <router-link to="/overviewQuiz" class="delete-btn"> Cancel </router-link> + <button class="submit-btn" type="submit"> Submit</button> + </div> + </form> + </div> + </body> + +</template> + +<style> +.new-quiz-page{ + margin:20vh; +} + +input{ + height: 25px; + width: 100%; +} +select{ + min-width: 100%; + height: 25px; + background-color: #E5E5E5; + border-color: transparent; + border-radius: 5px; +} + +.footer{ + margin: 10vh; + display: flex; + justify-content: center; + align-items: center; + +} + +</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 2132700773a2c7f725693d6cf702e6a600d0738b..bd3353a3973e7b839a24acc59d6957d09f4d96b0 100644 --- a/FullstackProsjekt/src/frontend/src/views/OverviewQuizView.vue +++ b/FullstackProsjekt/src/frontend/src/views/OverviewQuizView.vue @@ -1,15 +1,12 @@ <template> <body> <div class="overViewQuestion-page"> - <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> - <router-link to="/create-quiz" class="add-Btn">New quiz</router-link> - </div> + <router-link to="/dashboard" ><Svg name="go-back-icon" class="go-back-icon"/></router-link> + <h1>Your quizzes</h1> + <router-link to="/create-quiz" class="add-Btn">Create new quiz</router-link> + </div> + <p>Play, edit or delete a quiz saved from your profile</p> <div class="row"> <div class="quiz-div"> @@ -25,9 +22,11 @@ import QuizCard from "@/components/shared/create-quiz/QuizCard.vue"; import {getIdByToken} from "@/tokenController.js"; import {apiClient} from "@/api.js"; +import Svg from "@/assets/Svg.vue"; export default { components: { + Svg, QuizCard, }, data() { @@ -38,22 +37,21 @@ export default { }; }, mounted() { - this.populateQuizzes(); // Call populateQuizzes directly + this.populateQuizzes(); }, methods: { async populateQuizzes() { try { - await this.setUserId(); // Wait for setUserId to complete before fetching quizzes + await this.setUserId(); const response = await apiClient.get('/quiz/creator/' + this.userId); this.quizList = response.data; this.quizNo = this.quizList.length; } catch (error) { - // Handle errors this.errorMsg = 'Error retrieving quizzes'; } }, async setUserId() { - this.userId = await getIdByToken(); // Wait for getIdByToken() to resolve before assigning to this.userId + this.userId = await getIdByToken(); } } } @@ -63,7 +61,15 @@ export default { .overViewQuestion-page{ padding: 40px; } - +.headerDiv { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8vh; +} +.go-back-icon{ + margin-left: 1px; +} .quiz-div { display: flex; flex-wrap: wrap; @@ -71,12 +77,4 @@ export default { float: left; box-sizing: border-box; } - -.headerDiv { - display: flex; - width: 100%; - justify-content: space-between; -} - - </style> \ No newline at end of file diff --git a/FullstackProsjekt/src/frontend/src/views/PlayQuizView.vue b/FullstackProsjekt/src/frontend/src/views/PlayQuizView.vue index 2ec7c476d8a358f9aff4ffe6da537206d7cd02bf..78e6adf9bb94ba1e4c6274544a3e786faafbdb48 100644 --- a/FullstackProsjekt/src/frontend/src/views/PlayQuizView.vue +++ b/FullstackProsjekt/src/frontend/src/views/PlayQuizView.vue @@ -1,9 +1,172 @@ -<script > +<script> +import QuizResult from "@/components/shared/PlayQuiz/QuizResult.vue"; +import {getIdByToken} from "@/tokenController.js"; +import {apiClient} from "@/api.js"; + +export default { + data() { + return { + currentQuestion: null, + quizResult: null, + currentQuestionIndex: 0,//set to null + userId: 0, //set to null + quizId: 0, //set to null + quizTitle: 'Quiz title', + selectedOption: '', + questions: [], + currentQuestionText: '', + currentQuestionId: null, + currentAnswers: [], + quizResultId: 0, + errorMsg: '', + } + }, + beforeMount() { + this.getUser(); + }, + async mounted() { + await this.getUser(); + await this.setup(); + this.getQuestions(); + this.getQuiz(); + this.setCurrentQuestion(); + + }, + methods: { + async setup(){ + //TODO: QuizResultService + this.quizId = this.$route.params.quizId; + const data = { + quizId: this.quizId, + userId: this.userId + } + console.log(data) + try { + await apiClient.post('/results/create', data)//TODO: set QuizResultServiceId + } catch (error) { + this.errorMsg = 'Error starting quiz'; + } + }, + async getUser() { + this.userId = await getIdByToken(); + }, + getQuiz() { + try { + apiClient.get('/quiz/quiz/' + this.quizId).then(response => { + this.quizTitle = response.data.questions; + }); + } catch (error) { + this.errorMsg = 'Error retrieving quiz'; + } + }, + getQuestions(quizId) { + //TODO: use method to fetch questions + try { + apiClient.get('/questions/allQuestionsToAQuiz/' + this.quizId).then(response => { + this.questionIds = response.data; + }); + console.log(this.questionIds[0]) + } catch (error) { + this.errorMsg = 'Error retrieving questions'; + } + }, + getSampleQuestions() { + const quest = { + id: 0, + questionText: "question " + -1, + options: ["ans1","ans2", "ansThree"], + } + this.questions.push(quest); + for(let i=0; i<4; i++){ + const question = { + id: i, + questionText: "question " + i, + options: ["ans1","ans2"] + } + this.questions.push(question); + console.log(question.id); + console.log(this.questions.length); + } + }, + async nextQuestion() { + if(!this.selectedOption) { + this.errorMsg = 'You must select an answer'; + } else { + this.errorMsg = ''; + } + console.log(this.selectedOption); + + + console.log(this.selectedOption); + try { + const data = { + id: this.quizId, + questionId: this.currentQuestionId, + givenAnswer: this.selectedOption, + } + console.log(data) + await apiClient.post('/questions/save', data) + } catch (error) { + this.errorMsg = 'Cannot submit question'; + } + this.currentQuestionIndex++ + this.setCurrentQuestion(); + + }, + //setting current question, updating info + setCurrentQuestion() { + /*if(!this.questions.empty()){ + this.currentQuestion = this.questions[this.currentQuestionIndex]; + }*/ //for some reason this check messes up the app??? + this.currentQuestion = this.questions[this.currentQuestionIndex]; + //this.currentQuestionId = this.currentQuestion.id; + //this.currentQuestionText = this.currentQuestion.questionText; + //this.currentAnswers = this.currentQuestion.options; + }, + setupMCQuestion(question) { + this.questionTitle = question.questionText; + this.currentAnswers = question.options; + this.currentCorrectAnswer = question.answer; + }, + selectOption(option) { + this.selectedOption = option; + console.log(this.selectedOption); + } + } +} </script> <template> - + <div class="quiz"> + <div id="quiz-info"> + <h1 id="title">Play quiz</h1> + </div> + + <div id="current-q" class="question"> + + <h3 v-if="currentQuestion">{{currentQuestionText}}</h3> + <ul> + <li v-for="(option, index) in currentAnswers" :key="index"> + <label>{{ option }}</label> + <input type="radio" :id="option" :value="option" :checked="option === selectedOption" @change="selectOption(option)"> + </li> + <!-- + <li v-for="option in answersList"> + <label>test label</label>--> + <!-- + <input type="radio" :id="'option'" :value="option" + :checked="option === selectedOption">--> + <!--<label :for="'option'">option</label>--> + + </ul> + + + <button @click="nextQuestion">Next Question</button> + </div> + </div> + +<!-- <body> <div class="quiz"> @@ -43,7 +206,7 @@ </div> </body> - +--> </template> <style> diff --git a/FullstackProsjekt/src/frontend/src/views/ProfileView.vue b/FullstackProsjekt/src/frontend/src/views/ProfileView.vue index 75267bc70a3d628fa9adc8bcb316f935a850ab0b..f40f30a9f3c31834e2d4fcb6a8da8b1a68a45bcc 100644 --- a/FullstackProsjekt/src/frontend/src/views/ProfileView.vue +++ b/FullstackProsjekt/src/frontend/src/views/ProfileView.vue @@ -98,7 +98,6 @@ export default { <h3>{{ attempt.quizTitle }}</h3> <p>Score: {{ attempt.score }}</p> <p>Date: {{ attempt.date }}</p> - <!-- Add more details about each quiz attempt as needed --> </div> </div> </section> @@ -133,69 +132,112 @@ export default { </template> +<script> +import Svg from "@/assets/Svg.vue"; +import Modal from "@/components/shared/modal/Modal.vue" +import {ref} from 'vue' + +export default { + components: {Modal, Svg}, + data() { + return { + userId: null, + quizList:[], + showModal: ref(false), + isLoggedIn: true, + user: { + username: '', + } + }; + }, + mounted() { + this.user.username = localStorage.getItem('username'); + this.populateQuizzes(); + }, + computed: { + quizAttempts() { + return [ + { id: 1, quizTitle: 'Math Quiz', score: '80%', date: '2024-04-05' }, + { id: 2, quizTitle: 'Science Quiz', score: '90%', date: '2024-04-04' } + ]; + } + }, + methods:{ + logout(){ + this.isLoggedIn = false; + this.$router.push('/login'); + }, + closeModal(){ + this.showModal=false; + }, + async populateQuizzes() { + try { + await this.setUserId(); + const response = await apiClient.get('/quiz/creator/' + this.userId); + this.quizList = response.data; + } catch (error) { + console.error('Error retrieving quizzes:', error); + } + }, + async setUserId() { + this.userId = await getIdByToken(); + } + } +}; +</script> + + <style scoped> .profile { max-width: 800px; margin: 0 auto; padding: 30px; } - .profile-pic{ height: 150px; width: 150px; margin-right: 5vh; } - .user-info, .user-quizzes, .profile-options { margin-bottom: 40px; } - .user-details { display: flex; align-items: center; } - .user-details div { flex: 1; } - .user-quizzes .quiz { border: 1px solid #ccc; - background-color: #d7d7d7 ; + background-color: var(--field-placeholder) ; border-radius: 5px; padding: 10px; margin-bottom: 10px; cursor: pointer; } - .profile-options ul { list-style: none; padding: 0; } - .profile-options ul li { margin-bottom: 10px; } - .profile-options ul li a { text-decoration: none; - color: #CCA43B; + color: var(--option-color); } .profile-options ul li a:hover{ - color: #a2822e; + color: var(--option-hover); text-decoration: underline; } - - .progress-tracking { margin-bottom: 40px; } - .progress-tracking .quiz-attempt { border: 1px solid #ccc; border-radius: 5px; padding: 10px; margin-bottom: 10px; } - </style> diff --git a/FullstackProsjekt/src/frontend/src/views/SignupView.vue b/FullstackProsjekt/src/frontend/src/views/SignupView.vue index e5d314ded58cd9b2d042fbbb3a2c79f3b864beba..1883e5bcae0cb680bf6be70433ed82d470b4adfd 100644 --- a/FullstackProsjekt/src/frontend/src/views/SignupView.vue +++ b/FullstackProsjekt/src/frontend/src/views/SignupView.vue @@ -1,45 +1,3 @@ - -<script> - import Svg from '../assets/Svg.vue' - import {apiClient} from "@/api.js"; - export default { - name: 'Register', - components: {Svg}, - data(){ - return{ - username: '', - password:'', - password_confirm:'', - errorMsg: '', //TODO: display error to user - showPassword: false, - } - }, - methods:{ - async handleSubmit() { - //TODO: use interceptor to check matching password, send one password - try { - await apiClient.post('/auth/register', { - username: this.username, - password: this.password, - }).then(response => { - //TODO: display successful registration to user - alert("User: " + this.username + " created!") - this.$router.push('/login') - }); - } catch (error) { - //TODO: proper error handling - this.errorMsg = 'Error signing up, try again'; - } - }, - togglePasswordVisibility() { - this.showPassword = !this.showPassword; - }, - } - } -</script> - - - <template> <body> <div class="signupPage"> @@ -84,6 +42,43 @@ </template> +<script> +import Svg from '../assets/Svg.vue' +import {apiClient} from "@/api.js"; +export default { + name: 'Register', + components: {Svg}, + data(){ + return{ + username: '', + password:'', + password_confirm:'', + errorMsg: '', + showPassword: false, + } + }, + methods:{ + async handleSubmit() { + try { + await apiClient.post('/auth/register', { + username: this.username, + password: this.password, + }).then(response => { + alert("User: " + this.username + " created!") + this.$router.push('/login') + }); + } catch (error) { + this.errorMsg = 'Error signing up, try again'; + } + }, + togglePasswordVisibility() { + this.showPassword = !this.showPassword; + }, + } +} +</script> + + <style> .signupPage { padding-top: 10vh; @@ -91,7 +86,6 @@ justify-content: center; align-items: center; } - .signup { text-align: center; padding: 20px; @@ -99,17 +93,14 @@ align-items: center; border-style: solid; } - .signupBox { padding: 20px; } - .password-input { position: relative; display: inline-block; min-width: 300px; } - .showPasswordIcon { position: absolute; top: 50%; @@ -119,9 +110,7 @@ background: none; cursor: pointer; } - .submit-section{ margin-top: 5vh; } - </style>