Implement user creation, started project viewing
This commit is contained in:
parent
1925220823
commit
da45f0c633
8
pom.xml
8
pom.xml
@ -18,18 +18,10 @@
|
||||
<kotlin.version>1.4.31</kotlin.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
|
@ -1,11 +1,11 @@
|
||||
package uk.co.neviyn.projectplanner
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonBackReference
|
||||
import com.fasterxml.jackson.annotation.JsonManagedReference
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.CascadeType
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.GeneratedValue
|
||||
import javax.persistence.GenerationType
|
||||
import javax.persistence.Id
|
||||
import javax.persistence.JoinColumn
|
||||
import javax.persistence.JoinTable
|
||||
@ -14,42 +14,47 @@ import javax.persistence.ManyToOne
|
||||
import javax.persistence.OneToMany
|
||||
|
||||
@Entity
|
||||
class User(
|
||||
open class User(
|
||||
var username: String = "INVALID",
|
||||
var email: String = "INVALID",
|
||||
var password: String = "INVALID",
|
||||
@ManyToMany(cascade = [CascadeType.ALL])
|
||||
@JsonManagedReference
|
||||
@JsonIgnore
|
||||
@JoinTable(
|
||||
name = "Team",
|
||||
joinColumns = [JoinColumn(name = "user_id", referencedColumnName = "id")],
|
||||
inverseJoinColumns = [JoinColumn(name = "project_id", referencedColumnName = "id")]
|
||||
)
|
||||
var projects: Set<Project> = mutableSetOf(),
|
||||
@Id @GeneratedValue var id: Long? = null
|
||||
)
|
||||
var projects: MutableSet<Project> = mutableSetOf(),
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null
|
||||
) {}
|
||||
|
||||
@Entity
|
||||
class Project(
|
||||
var title: String = "INVALID",
|
||||
@ManyToMany(mappedBy = "projects")
|
||||
@JsonBackReference
|
||||
var members: Set<User> = mutableSetOf(),
|
||||
@JsonIgnore
|
||||
var members: MutableSet<User> = mutableSetOf(),
|
||||
@OneToMany(mappedBy = "project")
|
||||
var events: Set<Event> = mutableSetOf(),
|
||||
@Id @GeneratedValue var id: Long? = null
|
||||
@JsonIgnore
|
||||
var events: MutableSet<Event> = mutableSetOf(),
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null
|
||||
)
|
||||
|
||||
@Entity
|
||||
class Event(
|
||||
var title: String = "INVALID",
|
||||
var description: String = "INVALID",
|
||||
var start: LocalDateTime = LocalDateTime.MIN,
|
||||
var end: LocalDateTime = LocalDateTime.MIN,
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "project_id")
|
||||
@JsonIgnore
|
||||
var project: Project? = null,
|
||||
@ManyToMany(mappedBy = "events")
|
||||
var tags: Set<Tag> = mutableSetOf(),
|
||||
@Id @GeneratedValue var id: Long? = null
|
||||
@JsonIgnore
|
||||
var tags: MutableSet<Tag> = mutableSetOf(),
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null
|
||||
)
|
||||
|
||||
@Entity
|
||||
@ -59,12 +64,14 @@ class Comment(
|
||||
var user: User? = null,
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "event_id")
|
||||
@JsonIgnore
|
||||
var event: Event? = null,
|
||||
@ManyToMany(mappedBy = "comments")
|
||||
var tags: Set<Tag> = mutableSetOf(),
|
||||
@JsonIgnore
|
||||
var tags: MutableSet<Tag> = mutableSetOf(),
|
||||
var created: LocalDateTime = LocalDateTime.MIN,
|
||||
var comment: String = "INVALID",
|
||||
@Id @GeneratedValue var id: Long? = null
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null
|
||||
)
|
||||
|
||||
@Entity
|
||||
@ -76,13 +83,15 @@ class Tag(
|
||||
joinColumns = [JoinColumn(name = "tag_id", referencedColumnName = "id")],
|
||||
inverseJoinColumns = [JoinColumn(name = "comment_id", referencedColumnName = "id")]
|
||||
)
|
||||
var comments: Set<Comment> = mutableSetOf(),
|
||||
@JsonIgnore
|
||||
var comments: MutableSet<Comment> = mutableSetOf(),
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "event_tags",
|
||||
joinColumns = [JoinColumn(name = "tag_id", referencedColumnName = "id")],
|
||||
inverseJoinColumns = [JoinColumn(name = "event_id", referencedColumnName = "id")]
|
||||
)
|
||||
var events: Set<Event> = mutableSetOf(),
|
||||
@Id @GeneratedValue var id: Long? = null
|
||||
@JsonIgnore
|
||||
var events: MutableSet<Event> = mutableSetOf(),
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null
|
||||
)
|
@ -6,14 +6,37 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.ResponseBody
|
||||
import org.springframework.ui.Model
|
||||
|
||||
|
||||
import org.springframework.web.bind.annotation.ModelAttribute
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import javax.persistence.EntityManager
|
||||
import javax.transaction.Transactional
|
||||
|
||||
|
||||
@Controller
|
||||
class HtmlController @Autowired constructor(val userRepository: UserRepository, val projectRepository: ProjectRepository){
|
||||
class HtmlController @Autowired constructor(val userRepository: UserRepository, val projectRepository: ProjectRepository, val entityManager: EntityManager){
|
||||
|
||||
@GetMapping("/")
|
||||
fun landingPage(@AuthenticationPrincipal userDetails: CustomUserDetails?) : String {
|
||||
return if(userDetails == null) "landing"
|
||||
else "redirect:/projects"
|
||||
}
|
||||
|
||||
@GetMapping("/register")
|
||||
fun register(model: Model) : String {
|
||||
val user = User(username = "", email = "", password = "")
|
||||
model.addAttribute("user_details", user)
|
||||
return "register"
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
fun register(@ModelAttribute newUser: User) : String {
|
||||
newUser.password = passwordEncoder().encode(newUser.password)
|
||||
userRepository.save(newUser)
|
||||
return "login"
|
||||
}
|
||||
|
||||
@GetMapping("/login")
|
||||
fun login(model: Model, error: String?, logout: String?): String? {
|
||||
@ -22,24 +45,96 @@ class HtmlController @Autowired constructor(val userRepository: UserRepository,
|
||||
return "login"
|
||||
}
|
||||
|
||||
@GetMapping("/user/{id}")
|
||||
@ResponseBody
|
||||
fun getUser(@PathVariable id: Long) : User {
|
||||
return userRepository.findById(id).get()
|
||||
@GetMapping("/profile")
|
||||
fun getLoggedInUser(@AuthenticationPrincipal userDetails: CustomUserDetails, model: Model) : String {
|
||||
val user = DisplayUser(userDetails.user.id!!, userDetails.user.username, userDetails.user.email, "", "")
|
||||
model.addAttribute("userData", user)
|
||||
return "profile"
|
||||
}
|
||||
|
||||
@PreAuthorize("hasPermission(#projectID, 'Long', '')")
|
||||
@GetMapping("/project/{projectID}")
|
||||
@ResponseBody
|
||||
fun getProject(@PathVariable projectID: Long) : Project{
|
||||
return projectRepository.findById(projectID).get()
|
||||
@PostMapping("/profile")
|
||||
@Transactional
|
||||
fun updateLoggedInUser(@ModelAttribute userData: DisplayUser, @AuthenticationPrincipal userDetails: CustomUserDetails, model: Model) : String {
|
||||
if(userData.id == userDetails.user.id!! && passwordEncoder().matches(userData.oldPassword, userDetails.password)) {
|
||||
val user = userDetails.user
|
||||
user.email = userData.email
|
||||
if(userData.password.isNotEmpty()) user.password = passwordEncoder().encode(userData.password)
|
||||
userRepository.save(user)
|
||||
model.addAttribute("message", "Your profile has been updated")
|
||||
}
|
||||
else{
|
||||
model.addAttribute("error", "Incorrect existing password")
|
||||
}
|
||||
model.addAttribute("userData", DisplayUser(userData.id, userData.username, userData.email, userData.password, ""))
|
||||
return "profile"
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
@ResponseBody
|
||||
fun getLoggedInUser(@AuthenticationPrincipal userDetails: CustomUserDetails) : Long {
|
||||
return userDetails.user.id!!
|
||||
@PostMapping("/newproject")
|
||||
fun createNewProject(@RequestBody newProject: NewProject){
|
||||
val project = Project(title = newProject.name)
|
||||
projectRepository.save(project)
|
||||
}
|
||||
|
||||
@GetMapping("/projects")
|
||||
@Transactional
|
||||
fun listUserProjects(model: Model, @AuthenticationPrincipal userDetails: CustomUserDetails) : String {
|
||||
val user = entityManager.merge(userDetails.user) // Reattach User entity
|
||||
model.addAttribute("projects", user.projects)
|
||||
return "projectlist"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/project/{id}")
|
||||
class ProjectController @Autowired constructor(val projectRepository: ProjectRepository, val userRepository: UserRepository, val eventRepository: EventRepository) {
|
||||
|
||||
@GetMapping("")
|
||||
@PreAuthorize("hasPermission(#id, 'Long', '')")
|
||||
fun getProject(@PathVariable id: Long, model: Model) : String {
|
||||
val project = projectRepository.findById(id).get()
|
||||
model.addAttribute("project", project)
|
||||
return "project"
|
||||
}
|
||||
|
||||
@GetMapping("/events")
|
||||
@PreAuthorize("hasPermission(#id, 'Long', '')")
|
||||
fun getProjectEvents(@PathVariable id: Long) : Set<Event> {
|
||||
return projectRepository.findById(id).get().events
|
||||
}
|
||||
|
||||
@GetMapping("/adduser")
|
||||
@PreAuthorize("hasPermission(#id, 'Long', '')")
|
||||
fun addUserToProjectForm(@PathVariable id: Long, model: Model) : String{
|
||||
val project = projectRepository.findById(id).get()
|
||||
val users = userRepository.findByIdNotIn(project.members.map { it.id!! }).map { SimpleUser(it.id!!, it.username) }
|
||||
model.addAttribute("available_users", users)
|
||||
return "addprojectuser"
|
||||
}
|
||||
|
||||
@PostMapping("/adduser")
|
||||
@PreAuthorize("hasPermission(#id, 'Long', '')")
|
||||
fun addUserToProject(@PathVariable id: Long, @RequestBody u: UserID) {
|
||||
val user = userRepository.findById(u.id).get()
|
||||
val project = projectRepository.findById(id).get()
|
||||
project.members.add(user)
|
||||
projectRepository.save(project)
|
||||
}
|
||||
|
||||
@PostMapping("/removeuser")
|
||||
@PreAuthorize("hasPermission(#id, 'Long', '')")
|
||||
fun removeUserFromProject(@PathVariable id: Long, @RequestBody u: UserID) {
|
||||
val user = userRepository.findById(u.id).get()
|
||||
val project = projectRepository.findById(id).get()
|
||||
project.members.remove(user)
|
||||
projectRepository.save(project)
|
||||
}
|
||||
|
||||
@PostMapping("/addevent")
|
||||
@PreAuthorize("hasPermission(#id, 'Long', '')")
|
||||
fun addEventToProject(@PathVariable id: Long, @RequestBody e: NewEvent) {
|
||||
val project = projectRepository.findById(id).get()
|
||||
val event = Event(title = e.title, description = e.description, start = e.start, end = e.end, project = project)
|
||||
eventRepository.save(event)
|
||||
}
|
||||
}
|
@ -4,6 +4,8 @@ import org.springframework.data.repository.CrudRepository
|
||||
|
||||
interface UserRepository : CrudRepository<User, Long>{
|
||||
fun findByUsername(username: String): User?
|
||||
fun findByUsernameIsLike(partialUsername: String) : List<User>
|
||||
fun findByIdNotIn(ids: List<Long>) : List<User>
|
||||
}
|
||||
|
||||
interface ProjectRepository : CrudRepository<Project, Long>{}
|
||||
|
13
src/main/kotlin/uk/co/neviyn/projectplanner/Requests.kt
Normal file
13
src/main/kotlin/uk/co/neviyn/projectplanner/Requests.kt
Normal file
@ -0,0 +1,13 @@
|
||||
package uk.co.neviyn.projectplanner
|
||||
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class UserID(val id: Long)
|
||||
|
||||
data class SimpleUser(val id: Long, val username: String)
|
||||
|
||||
data class DisplayUser(val id: Long, val username: String, val email: String, val password: String, val oldPassword: String)
|
||||
|
||||
data class NewProject(val name: String)
|
||||
|
||||
data class NewEvent(val title: String, val description: String, val start: LocalDateTime, val end: LocalDateTime)
|
@ -25,10 +25,10 @@ import java.io.Serializable
|
||||
@Configuration
|
||||
class SecurityConfig @Autowired constructor(val userDetailsService: UserDetailsServiceImpl) : WebSecurityConfigurerAdapter() {
|
||||
override fun configure(http: HttpSecurity) {
|
||||
http.authorizeRequests().antMatchers("/").permitAll()
|
||||
http.authorizeRequests().antMatchers("/", "/*.jpg", "/*.svg", "/register").permitAll()
|
||||
.anyRequest().authenticated().and()
|
||||
.formLogin().loginPage("/login").permitAll().and()
|
||||
.logout().permitAll().and()
|
||||
.logout().logoutSuccessUrl("/").permitAll().and()
|
||||
.httpBasic()
|
||||
}
|
||||
|
||||
|
BIN
src/main/resources/static/landing_1.jpg
Normal file
BIN
src/main/resources/static/landing_1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 435 KiB |
26
src/main/resources/templates/addprojectuser.html
Normal file
26
src/main/resources/templates/addprojectuser.html
Normal file
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="fragments :: baseHeader(~{::title})">
|
||||
|
||||
<title>Add user to project | Project Planner</title>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js"></script>
|
||||
<script th:inline="javascript">
|
||||
const userList = [[${available_users}]];
|
||||
const options = {
|
||||
valueNames: ['id', 'username'],
|
||||
// Since there are no elements in the list, this will be used as template.
|
||||
item: '<li><h3 class="username"></h3></li>'
|
||||
};
|
||||
var list = new List('users', options, userList)
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="users">
|
||||
<label>Search
|
||||
<input class="search" placeholder="Search" />
|
||||
</label>
|
||||
<ul class="list"></ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,15 +1,40 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:fragment="baseHeader(title)">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<title th:replace="${title}">Base Title</title>
|
||||
|
||||
<!-- Common styles and scripts -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous">
|
||||
<link rel="shortcut icon" th:href="@{/images/favicon.ico}">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js" integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.0/font/bootstrap-icons.css">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div th:fragment="navbar">
|
||||
<form id="logoutForm" method="POST" th:action="@{/logout}">
|
||||
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
|
||||
</form>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">
|
||||
<i class="bi bi-calendar3 me-1"></i>Project Planner
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
|
||||
<div class="navbar-nav">
|
||||
<a class="nav-link" aria-current="page" href="/projects">Projects</a>
|
||||
<a class="nav-link" href="/profile">Profile</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-nav navbar-right">
|
||||
<a class="nav-link" onclick="document.forms['logoutForm'].submit()" style="cursor: pointer"><i class="bi bi-box-arrow-right me-1"></i>Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
33
src/main/resources/templates/landing.html
Normal file
33
src/main/resources/templates/landing.html
Normal file
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="fragments :: baseHeader(~{::title})">
|
||||
<title>Project Planner</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center mt-3">
|
||||
<div class="col text-center">
|
||||
<img th:src="@{/landing_1.jpg}" class="w-25 rounded-3" alt="landing page" src=""/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1 class="display-1 text-center">Project Planner</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<p class="lead text-center">
|
||||
Manage your project deadlines.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col text-center">
|
||||
<a href="/login" class="btn btn-primary btn-lg" role="button">Login</a>
|
||||
<a href="/register" class="btn btn-secondary btn-lg" role="button">Register</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -8,24 +8,36 @@
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center mt-3">
|
||||
<div class="col text-center">
|
||||
<h2 class="display-3">Welcome back</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center mt-3">
|
||||
<div class="col-8 text-center">
|
||||
<form class="form-sign_in" method="POST" th:action="@{/login}">
|
||||
<h2 class="form-heading">Log in</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group mt-3">
|
||||
<span th:text="${message}"></span>
|
||||
<label for="username">Username:</label>
|
||||
<input id="username" name="username" type="text" class="form-control" placeholder="Username" autofocus/>
|
||||
|
||||
<label for="password">Password:</label>
|
||||
<input id="password" name="password" type="password" class="form-control" placeholder="Password"/>
|
||||
<span class="has-error" th:text="${error}"></span>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" class="form-control form-control-lg" id="username" name="username" autofocus>
|
||||
<label for="username">Username</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input type="password" class="form-control form-control-lg" id="password" name="password">
|
||||
<label for="password">Password</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<span class="has-error text-danger" th:text="${error}"></span>
|
||||
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Log In</button>
|
||||
<h4 class="text-center"><a href="/registration">Create an account</a></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
56
src/main/resources/templates/profile.html
Normal file
56
src/main/resources/templates/profile.html
Normal file
@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="fragments :: baseHeader(~{::title})">
|
||||
<title>Profile | Project Planner</title>
|
||||
</head>
|
||||
<body>
|
||||
<div th:replace="fragments :: navbar"></div>
|
||||
<div class="container">
|
||||
<div class="row mt-3 justify-content-center">
|
||||
<div class="col">
|
||||
<h2 class="display-2 text-center mb-3">Profile</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form th:action="@{/profile}" th:object="${userData}" method="post">
|
||||
<div class="row justify-content-center mb-3">
|
||||
<div class="col-8">
|
||||
<input type="hidden" th:field="*{id}"/>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Username</span>
|
||||
<input aria-label="Username" class="form-control" readonly th:field="*{username}" type="text">
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Email</span>
|
||||
<input aria-label="Email" class="form-control" th:field="*{email}" type="text">
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Password</span>
|
||||
<input aria-label="Password" class="form-control" th:field="*{password}" type="password">
|
||||
</div>
|
||||
<p>Please enter your existing password to update your profile.</p>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Existing Password</span>
|
||||
<input aria-label="Existing Password" type="password" class="form-control form-control-lg"
|
||||
th:field="*{oldPassword}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-2 d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg">Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<span class="text-success" th:text="${message}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<span class="has-error text-danger" th:text="${error}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
96
src/main/resources/templates/project.html
Normal file
96
src/main/resources/templates/project.html
Normal file
@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="fragments :: baseHeader(~{::title})">
|
||||
<title th:text="${project.title} + ' | Project Planner'">Project Planner</title>
|
||||
</head>
|
||||
<head>
|
||||
<link href="https://cdn.jsdelivr.net/npm/fullcalendar@5.6.0/main.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.6.0/main.min.js" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
let calendarEl = document.getElementById('calendar');
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
let calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
themeSystem: 'bootstrap',
|
||||
initialView: 'dayGridMonth',
|
||||
bootstrapFontAwesome: false,
|
||||
fixedWeekCount: false,
|
||||
editable: true,
|
||||
nowIndicator: true,
|
||||
aspectRatio: 2.2,
|
||||
eventAdd(addInfo) {
|
||||
console.log(addInfo.event)
|
||||
}
|
||||
});
|
||||
calendar.render();
|
||||
};
|
||||
</script>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<div th:replace="fragments :: navbar"></div>
|
||||
<div class="container-fluid">
|
||||
<div class="row mt-3 justify-content-center">
|
||||
<div class="col">
|
||||
<h2 class="display-2 text-center mb-3" th:text="${project.title}">Project Name</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-2">
|
||||
<div class="list-group mb-3">
|
||||
<button class="list-group-item active" data-bs-toggle="modal" data-bs-target="#newEventModal">Add Event...</button>
|
||||
</div>
|
||||
<div class="list-group">
|
||||
<div class="list-group-item active">Members:</div>
|
||||
<div th:each="member : ${project.members}">
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<p th:text="${member.username}">username</p>
|
||||
<a th:href="@{/{pid}/removeuser/{uid}(pid=${id},uid=${member.id})}">
|
||||
<i class="bi bi-x-circle text-danger"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a th:href="@{/{id}/adduser(id=${id})}" class="list-group-item list-group-item-action list-group-item-secondary">Add member</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div id="calendar" class="d-flex flex-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="newEventModal" class="modal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">New Event</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" class="form-control" id="titleInput">
|
||||
<label for="titleInput">Event Title</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<textarea class="form-control h-100" id="descriptionInput" rows="3"></textarea>
|
||||
<label for="descriptionInput">Event Description</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input type="datetime-local" class="form-control" id="startTimeInput">
|
||||
<label for="startTimeInput">Start Time</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input type="datetime-local" class="form-control" id="endTimeInput">
|
||||
<label for="endTimeInput">End Time</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary">Add Event</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
28
src/main/resources/templates/projectlist.html
Normal file
28
src/main/resources/templates/projectlist.html
Normal file
@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="fragments :: baseHeader(~{::title})">
|
||||
<title>Projects | Project Planner</title>
|
||||
</head>
|
||||
<body>
|
||||
<form id="logoutForm" method="POST" th:action="@{/logout}">
|
||||
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
|
||||
</form>
|
||||
<div th:replace="fragments :: navbar"></div>
|
||||
<div class="container">
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
<h2 class="display-2 text-center mb-3">My Projects</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-xl-6 col-md-8 col-sm-12">
|
||||
<div class="list-group" th:each="project : ${projects}">
|
||||
<!--suppress ThymeleafVariablesResolveInspection -->
|
||||
<a class="list-group-item list-group-item-action" th:text="${project.title}" th:href="@{~/project/{id}(id=${project.id})}">Title
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
64
src/main/resources/templates/register.html
Normal file
64
src/main/resources/templates/register.html
Normal file
@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="fragments :: baseHeader(~{::title})">
|
||||
<title>Register | Project Planner</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
<h2 class="display-2 text-center">Create Account</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center mt-3">
|
||||
<div class="col-6 text-center">
|
||||
<form th:action="@{/register}" th:object="${user_details}" method="post" class="needs-validation" novalidate>
|
||||
<div class="mb-3">
|
||||
<label for="emailInput" class="form-label">Email address</label>
|
||||
<input type="email" class="form-control form-control-lg" id="emailInput" th:field="*{email}" required/>
|
||||
<div class="invalid-feedback">
|
||||
Please enter an email address.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="usernameInput" class="form-label">Username</label>
|
||||
<input type="text" class="form-control form-control-lg" id="usernameInput" th:field="*{username}" required/>
|
||||
<div class="invalid-feedback">
|
||||
Please enter a username.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="passwordInput" class="form-label">Password</label>
|
||||
<input type="password" class="form-control form-control-lg" id="passwordInput" th:field="*{password}" required/>
|
||||
<div class="invalid-feedback">
|
||||
Please enter a password.
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-lg">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
'use strict'
|
||||
|
||||
// Fetch all the forms we want to apply custom Bootstrap validation styles to
|
||||
var forms = document.querySelectorAll('.needs-validation')
|
||||
|
||||
// Loop over them and prevent submission
|
||||
Array.prototype.slice.call(forms)
|
||||
.forEach(function (form) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
form.classList.add('was-validated')
|
||||
}, false)
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user