Add OnlineQuiz app

This commit is contained in:
2025-10-26 19:35:59 +01:00
parent bd757d5ceb
commit aae358b2be
19 changed files with 1367 additions and 6 deletions

28
OnlineQuiz/build.gradle Normal file
View File

@@ -0,0 +1,28 @@
import org.springframework.boot.gradle.plugin.SpringBootPlugin
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
description = 'Secure Online Quiz Application'
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
repositories {
mavenCentral()
}
dependencyManagement {
imports {
mavenBom SpringBootPlugin.BOM_COORDINATES
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
}

View File

@@ -0,0 +1,13 @@
package com.app.quiz;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OnlineQuizApplication {
public static void main(String[] args) {
SpringApplication.run(OnlineQuizApplication.class, args);
}
}

View File

@@ -0,0 +1,48 @@
package com.app.quiz.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(request -> request
.requestMatchers("/login","/register").permitAll()
.requestMatchers("/home").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login").permitAll()
.defaultSuccessUrl("/home")
)
.logout(Customizer.withDefaults());
return http.build();
}
@Bean
public AuthenticationManager authenticationManager() {
return authentication ->
new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials());
}
}

View File

@@ -0,0 +1,222 @@
package com.app.quiz.controller;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.ui.Model;
import org.springframework.security.core.Authentication;
import com.app.quiz.service.QuizUserDetailsService;
import com.app.quiz.model.Question;
import com.app.quiz.service.QuestionsService;
@Controller
public class QuizController {
private final QuizUserDetailsService userDetailsService;
private final QuestionsService questionsService;
private final AuthenticationManager authenticationManager;
public QuizController(QuizUserDetailsService userDetailsService, AuthenticationManager authenticationManager, QuestionsService questionsService) {
this.userDetailsService = userDetailsService;
this.authenticationManager = authenticationManager;
this.questionsService = questionsService;
}
@GetMapping("/home")
public String homepage(Model model) {
// Get the authenticated user's details
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Get the username
String username = authentication.getName();
model.addAttribute("username", username);
// Get the user's role
String role = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.findFirst()
.orElse("ROLE_USER"); // Default role if no authority is found
// Redirect to the appropriate page based on the role
if (role.equals("ROLE_ADMIN")) {
// Fetch the latest quizzes from the service
List<Question> quizzes = questionsService.loadQuizzes();
// Add the quizzes to the model
model.addAttribute("quizzes", quizzes);
return "QuizList"; // Return the QuizList.html template
} else {
// Fetch the latest quizzes from the service
List<Question> quizzes = questionsService.loadQuizzes();
// Add the quizzes to the model
model.addAttribute("quizzes", quizzes);
return "Quiz"; // Return the Quiz.html template
}
}
@GetMapping("/login")
public String login() {
return "login"; // Returns the login.html template
}
@GetMapping("/register")
public String register() {
return "register"; // Returns the register.html template
}
// POST endpoint to handle user registration and auto-login
@PostMapping("/register")
public String registerUser(
@RequestParam String username, // Username from the form
@RequestParam String password, // Password from the form
@RequestParam String email, // Email from the form
@RequestParam String role, // Role from the form,
HttpServletRequest request
) {
// Register the user by storing their details in the HashMap
try {
userDetailsService.registerUser(username, password, email, role);
} catch (Exception userExistsAlready) {
// Redirect to the /register endpoint
return "redirect:/register?error";
}
// Authenticate the user
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password);
authToken.setDetails(new WebAuthenticationDetails(request));
Authentication authentication = authenticationManager.authenticate(authToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
// Redirect to the /login endpoint
return "redirect:/login?success";
}
@GetMapping("/addQuiz")
public String showAddQuizForm(Model model) {
model.addAttribute("quiz", new Question()); // Add a new Quiz object to the model
return "addQuiz"; // Return the addQuiz.html template
}
@PostMapping("/addQuiz")
public String addQuiz(@ModelAttribute Question quiz, Model model, Authentication authentication) {
// Get the user's role
String role = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.findFirst()
.orElse("ROLE_USER"); // Default role if no authority is found
// Redirect to the appropriate page based on the role
if (role.equals("ROLE_ADMIN")) {
quiz.setId(questionsService.getNextId());
// Add the quiz to the service
questionsService.addQuestion(quiz);
// Add a success message to the model
model.addAttribute("success", "Quiz added successfully!");
// Redirect to the quiz list page
return "redirect:/home";
} else {
// Add an error message to the model
model.addAttribute("error", "You do not have permission to add a quiz.");
// Redirect to the add quiz page
return "redirect:/addQuiz?error";
}
}
// Display the edit quiz page
@GetMapping("/editQuiz/{id}")
public String showEditQuizForm(@PathVariable("id") int id, Model model) {
// Find the quiz by ID
Question quiz = questionsService.getQuestionById(id);
// Add the quiz to the model
model.addAttribute("quiz", quiz);
// Return the editQuiz.html template
return "editQuiz";
}
@PostMapping("/editQuestion")
public String editQuestion(@ModelAttribute("quiz") Question quiz) {
// Get the authenticated user's details
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Get the user's role
String role = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.findFirst()
.orElse("ROLE_USER"); // Default role if no authority is found
// Redirect to the appropriate page based on the role
if (role.equals("ROLE_ADMIN")) {
// Update the quiz in the service
questionsService.editQuestion(quiz);
// Redirect to the quiz list page
return "redirect:/home";
} else {
// Redirect to the quiz page
return "redirect:/home";
}
}
@GetMapping("/deleteQuiz/{id}")
public String deleteQuiz(@PathVariable("id") int id, Model model) {
// Get the authenticated user's details
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Get the user's role
String role = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.findFirst()
.orElse("ROLE_USER"); // Default role if no authority is found
// Redirect to the appropriate page based on the role
if (role.equals("ROLE_ADMIN")) {
// Delete the quiz by ID
questionsService.deleteQuestion(id);
return "redirect:/home"; // Redirect to the quiz list page
} else {
return "redirect:/home"; // Redirect to the home page
}
}
@PostMapping("/submitQuiz")
public String evaluateQuiz(@RequestParam Map<String, String> allParams, Model model) {
int correctAnswers = 0;
List<String> userAnswers = new ArrayList<>();
List<Question> quizzes = questionsService.loadQuizzes();
// Iterate through the quizzes and compare answers
for (int i = 0; i < quizzes.size(); i++) {
String userAnswer = allParams.get("answer" + i); // Get the answer for question i
userAnswers.add(userAnswer); // Store user's answer
if (quizzes.get(i).getCorrectAnswer().equals(userAnswer)) {
correctAnswers++;
}
}
// Add data to the model
model.addAttribute("quizzes", quizzes);
model.addAttribute("userAnswers", userAnswers);
model.addAttribute("correctAnswers", correctAnswers);
model.addAttribute("totalQuestions", quizzes.size());
// Return the result template
return "result";
}
}

View File

@@ -0,0 +1,75 @@
package com.app.quiz.model;
import java.util.ArrayList;
import java.util.Arrays;
public class Question {
private Integer id;
private String questionText;
private ArrayList<String> options; // Keep options as ArrayList<String>
private String correctAnswer;
// No-argument constructor (required for Thymeleaf binding)
public Question() {
this.options = new ArrayList<>();
}
// Constructor with parameters
public Question(Integer id, String questionText, ArrayList<String> options, String correctAnswer) {
this.id = id;
this.questionText = questionText;
this.options = options;
this.correctAnswer = correctAnswer;
}
// Getters and Setters
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getQuestionText() {
return questionText;
}
public void setQuestionText(String questionText) {
this.questionText = questionText;
}
public ArrayList<String> getOptions() {
return options;
}
public void setOptions(ArrayList<String> options) {
this.options = options;
}
// Helper method to get options as a comma-separated string
public String getOptionsAsString() {
return String.join(",", options);
}
// Helper method to set options from a comma-separated string
public void setOptionsFromString(String optionsString) {
this.options = new ArrayList<>(Arrays.asList(optionsString.split(",")));
}
public String getCorrectAnswer() {
return correctAnswer;
}
public void setCorrectAnswer(String correctAnswer) {
this.correctAnswer = correctAnswer;
}
@Override
public String toString() {
return ("ID: " + id +
"\nQuestion: " + questionText +
"\nOptions: " + options +
"\nCorrect Answer: " + correctAnswer);
}
}

View File

@@ -0,0 +1,56 @@
package com.app.quiz.model;
public class User {
private String username;
private String email;
private String password;
private String role;
public User(String username, String email, String password, String role) {
this.username = username;
this.email = email;
this.password = password;
this.role = role;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", email='" + email + '\'' +
", role='" + role + '\'' +
'}';
}
}

View File

@@ -0,0 +1,69 @@
package com.app.quiz.service;
import java.util.*;
import org.springframework.stereotype.Service;
import com.app.quiz.model.Question;
@Service
public class QuestionsService {
private final Map<Integer, Question> questions = new HashMap<>();
private int nextId = 1;
public List<Question> loadQuizzes() {
return List.copyOf(questions.values());
}
public Question getQuestionById(int id) {
return questions.get(id);
}
public int getNextId() {
return nextId++;
}
public boolean addQuestion(Question Question) {
Integer QuestionId = Question.getId();
if (questions.containsKey(QuestionId)) {
return false;
} else {
questions.put(QuestionId, Question);
return true;
}
}
public boolean editQuestion(Question Question) {
Integer QuestionId = Question.getId();
if (questions.containsKey(QuestionId)) {
questions.put(QuestionId, Question);
return true;
} else {
return false;
}
}
public boolean deleteQuestion(int id) {
Integer QuestionId = Integer.valueOf(id);
if (questions.containsKey(QuestionId)) {
questions.remove(QuestionId);
return true;
} else {
return false;
}
}
public int submitQuestion(ArrayList<Question> list) {
int result = 0;
for (Question Question: list){
Question QuestionInList = questions.get(Question.getId());
if(QuestionInList.getCorrectAnswer().equals(Question.getCorrectAnswer())) {
result++;
}
}
return result;
}
}

View File

@@ -0,0 +1,45 @@
package com.app.quiz.service;
import com.app.quiz.model.User;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
public class QuizUserDetailsService implements UserDetailsService {
private final List<User> users = new ArrayList<>();
private final PasswordEncoder passwordEncoder;
public QuizUserDetailsService(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<User> user = users.stream().filter(u -> u.getUsername().equals(username)).findFirst();
if (user.isEmpty()) {
throw new UsernameNotFoundException("User %s not found".formatted(username));
}
return org.springframework.security.core.userdetails.User
.withUsername(user.get().getUsername())
.password(user.get().getPassword())
.roles(user.get().getRole())
.build();
}
public void registerUser(String username, String password, String email, String role) {
if (users.stream().anyMatch(u -> u.getUsername().equals(username))) {
throw new BadCredentialsException("Username already exists");
}
users.add(new User(username, email, passwordEncoder.encode(password), role));
}
}

View File

@@ -0,0 +1,9 @@
spring.application.name=Online Quiz
# Server port
server.port=8080
# Thymeleaf configuration
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Quiz</title>
<style>
.quiz-container {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ccc;
border-radius: 5px;
}
/* Header styles */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #f4f4f4;
border-bottom: 1px solid #ccc;
}
.question {
font-weight: bold;
margin-bottom: 10px;
}
.options {
margin-left: 20px;
}
.submit-button {
margin-top: 20px;
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.submit-button:hover {
background-color: #45a049;
}
/* Logout button styles */
.logout-button {
background-color: #2196F3; /* Blue background */
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.logout-button:hover {
background-color: #1976D2; /* Darker blue on hover */
}
</style>
</head>
<body>
<!-- Header section with Logout Button -->
<div class="header">
<h1>Quiz Questions</h1>
<form th:action="@{/logout}" method="post">
<button type="submit" class="logout-button">Logout</button>
</form>
</div>
<form th:action="@{/submitQuiz}" method="post">
<div th:each="quiz, iterStat : ${quizzes}" class="quiz-container">
<div class="question" th:text="${'Question ' + (iterStat.index + 1) + ': ' + quiz.questionText}"></div>
<div class="options">
<ul>
<li th:each="option, optStat : ${quiz.options}">
<input type="radio" th:name="${'answer' + iterStat.index}" th:value="${option}" th:id="${'option' + iterStat.index + '-' + optStat.index}" required>
<label th:for="${'option' + iterStat.index + '-' + optStat.index}" th:text="${option}"></label>
</li>
</ul>
</div>
</div>
<button type="submit" class="submit-button">Submit Answers</button>
</form>
</body>
</html>

View File

@@ -0,0 +1,124 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Quiz List</title>
<style>
/* General styles */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
/* Header styles */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #f4f4f4;
border-bottom: 1px solid #ccc;
}
/* Quiz container styles */
.quiz-container {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ccc;
border-radius: 5px;
}
.question {
font-weight: bold;
margin-bottom: 10px;
}
.options {
margin-left: 20px;
}
.correct-answer {
color: green;
font-weight: bold;
}
/* Button styles */
.add-quiz-button, .edit-quiz-button, .delete-quiz-button {
margin-top: 20px;
padding: 10px 20px;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.add-quiz-button {
background-color: #4CAF50;
}
.edit-quiz-button {
background-color: #2196F3;
}
.delete-quiz-button {
background-color: #f44336;
}
.add-quiz-button:hover, .edit-quiz-button:hover, .delete-quiz-button:hover {
opacity: 0.8;
}
/* Logout button styles */
.logout-button {
background-color: #2196F3; /* Blue background */
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.logout-button:hover {
background-color: #1976D2; /* Darker blue on hover */
}
</style>
</head>
<body>
<!-- Header section with Logout Button -->
<div class="header">
<h1>Quiz Questions</h1>
<form th:action="@{/logout}" method="post">
<button type="submit" class="logout-button">Logout</button>
</form>
</div>
<!-- Add Quiz Button -->
<div style="padding: 20px;">
<a th:href="@{/addQuiz}">
<button class="add-quiz-button">Add Quiz</button>
</a>
</div>
<!-- Quiz List -->
<div style="padding: 20px;">
<div th:each="quiz : ${quizzes}" class="quiz-container">
<div class="question" th:text="${'Question: ' + quiz.questionText}"></div>
<div class="options">
<ul>
<li th:each="option : ${quiz.options}" th:text="${option}"></li>
</ul>
</div>
<div class="correct-answer" th:text="${'Correct Answer: ' + quiz.correctAnswer}"></div>
<!-- Edit Button -->
<a th:href="@{/editQuiz/{id}(id=${quiz.id})}">
<button class="edit-quiz-button">Edit Quiz</button>
</a>
<!-- Delete Button -->
<a th:href="@{/deleteQuiz/{id}(id=${quiz.id})}">
<button class="delete-quiz-button">Delete Quiz</button>
</a>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,123 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Add Quiz</title>
<style>
/* General styles */
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
/* Form container */
.form-container {
background-color: #fff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
max-width: 500px;
width: 100%;
}
/* Form header */
.form-header {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
text-align: center;
}
/* Form group (label + input) */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #555;
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
box-sizing: border-box;
}
.form-group input:focus {
border-color: #2196F3;
outline: none;
box-shadow: 0 0 5px rgba(33, 150, 243, 0.5);
}
/* Error messages */
.error-message {
color: #f44336;
font-size: 14px;
margin-top: 5px;
}
/* Submit button */
.submit-button {
width: 100%;
padding: 12px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
.submit-button:hover {
background-color: #1976D2;
}
</style>
</head>
<body>
<div class="form-container">
<!-- Form header -->
<div class="form-header">Add a New Quiz</div>
<!-- Form -->
<form th:action="@{/addQuiz}" method="post" th:object="${quiz}">
<!-- Question -->
<div class="form-group">
<label for="questionText">Question:</label>
<input type="text" id="questionText" th:field="*{questionText}" required>
<span th:if="${#fields.hasErrors('questionText')}" th:errors="*{questionText}" class="error-message"></span>
</div>
<!-- Options -->
<div class="form-group">
<label for="options">Options (comma-separated):</label>
<input type="text" id="options" th:field="*{options}" required>
<span th:if="${#fields.hasErrors('options')}" th:errors="*{options}" class="error-message"></span>
</div>
<!-- Correct Answer -->
<div class="form-group">
<label for="correctAnswer">Correct Answer:</label>
<input type="text" id="correctAnswer" th:field="*{correctAnswer}" required>
<span th:if="${#fields.hasErrors('correctAnswer')}" th:errors="*{correctAnswer}" class="error-message"></span>
</div>
<!-- Submit button -->
<button type="submit" class="submit-button">Add Quiz</button>
</form>
</div>
</body>
</html>

View File

@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Edit Quiz</title>
<style>
/* General styles */
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
/* Form container */
.form-container {
background-color: #fff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
max-width: 500px;
width: 100%;
}
/* Form header */
.form-header {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
text-align: center;
}
/* Form group (label + input) */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #555;
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
box-sizing: border-box;
}
.form-group input:focus {
border-color: #2196F3;
outline: none;
box-shadow: 0 0 5px rgba(33, 150, 243, 0.5);
}
/* Error messages */
.error-message {
color: #f44336;
font-size: 14px;
margin-top: 5px;
}
/* Submit button */
.submit-button {
width: 100%;
padding: 12px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
.submit-button:hover {
background-color: #1976D2;
}
</style>
</head>
<body>
<div class="form-container">
<!-- Form header -->
<div class="form-header">Edit Quiz</div>
<!-- Form -->
<form th:action="@{/editQuestion}" method="post" th:object="${quiz}">
<!-- Hidden field for quiz ID -->
<input type="hidden" th:field="*{id}" />
<!-- Question -->
<div class="form-group">
<label for="questionText">Question:</label>
<input type="text" id="questionText" th:field="*{questionText}" required>
<span th:if="${#fields.hasErrors('questionText')}" th:errors="*{questionText}" class="error-message"></span>
</div>
<!-- Options -->
<div class="form-group">
<label for="options">Options (comma-separated):</label>
<input type="text" id="options" th:value="${quiz.getOptionsAsString()}" name="options" required>
<span th:if="${#fields.hasErrors('options')}" th:errors="*{options}" class="error-message"></span>
</div>
<!-- Correct Answer -->
<div class="form-group">
<label for="correctAnswer">Correct Answer:</label>
<input type="text" id="correctAnswer" th:field="*{correctAnswer}" required>
<span th:if="${#fields.hasErrors('correctAnswer')}" th:errors="*{correctAnswer}" class="error-message"></span>
</div>
<!-- Submit button -->
<button type="submit" class="submit-button">Update Quiz</button>
</form>
</div>
</body>
</html>

View File

@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Page</title>
<style>
/* Center the body content */
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
/* Style the login container */
.login-container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 300px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center; /* Center items horizontally */
}
/* Style the form elements */
.login-container h2 {
margin-bottom: 20px;
}
.login-container input {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ccc;
border-radius: 4px;
max-width: 250px; /* Limit the width of inputs */
}
.login-container button {
width: 100%;
padding: 10px;
background-color: #2d2db0;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
max-width: 250px; /* Limit the width of the button */
}
.login-container button:hover {
background-color: #2d2db0;
}
/* Error and Success Messages */
.error {
color: red;
margin-top: 10px;
}
.success {
color: green;
margin-top: 10px;
}
/* Link to Registration Page */
.login-container p {
margin-top: 20px;
}
.login-container a {
color: #2d2db0;
text-decoration: none;
}
.login-container a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="login-container">
<h2>Login</h2>
<!-- Login Form -->
<form th:action="@{/login}" method="post" class="form-group">
<!-- Username -->
<input type="text" name="username" placeholder="Username" required>
<!-- Password -->
<input type="password" name="password" placeholder="Password" required>
<!-- Submit Button -->
<button type="submit">Login</button>
</form>
<!-- Error Message -->
<div th:if="${param.error}" class="error">
Invalid username or password.
</div>
<!-- Success Message -->
<div th:if="${param.success}" class="success">
User registered successfully!
</div>
<!-- Link to Registration Page -->
<p>Don't have an account? <a href="/register">Register here</a></p>
</div>
</body>
</html>

View File

@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Registration Page</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.registration-container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 300px;
text-align: center;
}
.registration-container h2 {
margin-bottom: 20px;
}
.registration-container input, .registration-container select {
width: 80%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
.registration-container select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 1em;
}
.registration-container button {
width: 80%;
padding: 10px;
background-color: #2d2db0;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
.registration-container button:hover {
background-color: #2d2db0;
}
.error {
color: red;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="registration-container">
<h2>Register</h2>
<form th:action="@{/register}" method="post" class="form-group">
<!-- Username -->
<input type="text" name="username" placeholder="Username" required>
<!-- Password -->
<input type="password" name="password" placeholder="Password" required>
<!-- Email -->
<input type="email" name="email" placeholder="Email" required>
<!-- Role Dropdown -->
<select name="role" required>
<option value="" disabled selected>Select your role</option>
<option value="ADMIN">Admin</option>
<option value="USER">User</option>
</select>
<!-- Submit Button -->
<button type="submit">Register</button>
</form>
<!-- Error Message -->
<div th:if="${param.error}" class="error">
Invalid username.
</div>
<p>Already have an account? <a href="/login">Login here</a></p>
</div>
</body>
</html>

View File

@@ -0,0 +1,131 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Quiz Result</title>
<style>
/* General styles */
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
/* Header styles */
.header {
position: fixed;
top: 0;
right: 0;
padding: 10px 20px;
display: flex;
gap: 10px; /* Space between buttons */
}
/* Button styles */
.nav-button {
background-color: #2196F3; /* Blue background */
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
text-decoration: none; /* Remove underline for links */
font-size: 14px;
}
.nav-button:hover {
background-color: #1976D2; /* Darker blue on hover */
}
/* Result container styles */
.result-container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
max-width: 600px;
width: 100%;
text-align: center;
margin: 80px auto 20px; /* Add margin to avoid overlap with header */
}
.result-header {
font-size: 24px;
font-weight: bold;
color: #4CAF50;
margin-bottom: 20px;
}
.result-details {
margin-top: 20px;
text-align: left;
}
.result-details h3 {
margin-bottom: 10px;
color: #333;
}
.result-details ul {
list-style-type: none;
padding: 0;
}
.result-details li {
margin-bottom: 10px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.correct {
color: #4CAF50;
}
.incorrect {
color: #f44336;
}
</style>
</head>
<body>
<!-- Header with Home and Logout Buttons -->
<div class="header">
<a th:href="@{/home}">
<button class="nav-button">Home</button>
</a>
<form th:action="@{/logout}" method="post">
<button type="submit" class="nav-button">Logout</button>
</form>
</div>
<!-- Result Container -->
<div class="result-container">
<!-- Result Header -->
<div class="result-header" th:text="${'You scored ' + correctAnswers + ' out of ' + totalQuestions + '!'}"></div>
<!-- Result Details -->
<div class="result-details">
<h3>Details:</h3>
<ul>
<!-- Iterate through quizzes and display correct/incorrect answers -->
<li th:each="quiz, iterStat : ${quizzes}">
<div>
<strong th:text="${'Question ' + (iterStat.index + 1) + ': ' + quiz.questionText}"></strong>
</div>
<div>
<span>Your Answer: </span>
<span th:class="${quiz.correctAnswer == userAnswers[iterStat.index] ? 'correct' : 'incorrect'}"
th:text="${userAnswers[iterStat.index]}"></span>
</div>
<div th:if="${quiz.correctAnswer != userAnswers[iterStat.index]}">
<span>Correct Answer: </span>
<span class="correct" th:text="${quiz.correctAnswer}"></span>
</div>
</li>
</ul>
</div>
</div>
</body>
</html>

View File

@@ -1,7 +1,9 @@
plugins {
id 'org.springframework.boot' version '3.5.7' apply false
id 'io.spring.dependency-management' version '1.1.7' apply false
}
allprojects { allprojects {
group = 'me.ahlroos' group = 'me.ahlroos'
version = '1.0-SNAPSHOT' version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
} }

2
gradlew vendored
View File

@@ -45,7 +45,7 @@
# by Bash, Ksh, etc; in particular arrays are avoided. # by Bash, Ksh, etc; in particular arrays are avoided.
# #
# The "traditional" practice of packing multiple parameters into a # The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security # space-separated string is a well documented source of bugs and config
# problems, so this is (mostly) avoided, by progressively accumulating # problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java. # options in "$@", and eventually passing that to Java.
# #

View File

@@ -1,3 +1,4 @@
rootProject.name = 'java-developer-course' rootProject.name = 'java-developer-course'
include 'PetCareScheduler' include 'PetCareScheduler'
include 'Portfolio' include 'Portfolio'
include 'OnlineQuiz'