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>
|
<kotlin.version>1.4.31</kotlin.version>
|
||||||
</properties>
|
</properties>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-data-jdbc</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-security</artifactId>
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
package uk.co.neviyn.projectplanner
|
package uk.co.neviyn.projectplanner
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonBackReference
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
import com.fasterxml.jackson.annotation.JsonManagedReference
|
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import javax.persistence.CascadeType
|
import javax.persistence.CascadeType
|
||||||
import javax.persistence.Entity
|
import javax.persistence.Entity
|
||||||
import javax.persistence.GeneratedValue
|
import javax.persistence.GeneratedValue
|
||||||
|
import javax.persistence.GenerationType
|
||||||
import javax.persistence.Id
|
import javax.persistence.Id
|
||||||
import javax.persistence.JoinColumn
|
import javax.persistence.JoinColumn
|
||||||
import javax.persistence.JoinTable
|
import javax.persistence.JoinTable
|
||||||
@ -14,42 +14,47 @@ import javax.persistence.ManyToOne
|
|||||||
import javax.persistence.OneToMany
|
import javax.persistence.OneToMany
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
class User(
|
open class User(
|
||||||
var username: String = "INVALID",
|
var username: String = "INVALID",
|
||||||
var email: String = "INVALID",
|
var email: String = "INVALID",
|
||||||
var password: String = "INVALID",
|
var password: String = "INVALID",
|
||||||
@ManyToMany(cascade = [CascadeType.ALL])
|
@ManyToMany(cascade = [CascadeType.ALL])
|
||||||
@JsonManagedReference
|
@JsonIgnore
|
||||||
@JoinTable(
|
@JoinTable(
|
||||||
name = "Team",
|
name = "Team",
|
||||||
joinColumns = [JoinColumn(name = "user_id", referencedColumnName = "id")],
|
joinColumns = [JoinColumn(name = "user_id", referencedColumnName = "id")],
|
||||||
inverseJoinColumns = [JoinColumn(name = "project_id", referencedColumnName = "id")]
|
inverseJoinColumns = [JoinColumn(name = "project_id", referencedColumnName = "id")]
|
||||||
)
|
)
|
||||||
var projects: Set<Project> = mutableSetOf(),
|
var projects: MutableSet<Project> = mutableSetOf(),
|
||||||
@Id @GeneratedValue var id: Long? = null
|
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null
|
||||||
)
|
) {}
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
class Project(
|
class Project(
|
||||||
var title: String = "INVALID",
|
var title: String = "INVALID",
|
||||||
@ManyToMany(mappedBy = "projects")
|
@ManyToMany(mappedBy = "projects")
|
||||||
@JsonBackReference
|
@JsonIgnore
|
||||||
var members: Set<User> = mutableSetOf(),
|
var members: MutableSet<User> = mutableSetOf(),
|
||||||
@OneToMany(mappedBy = "project")
|
@OneToMany(mappedBy = "project")
|
||||||
var events: Set<Event> = mutableSetOf(),
|
@JsonIgnore
|
||||||
@Id @GeneratedValue var id: Long? = null
|
var events: MutableSet<Event> = mutableSetOf(),
|
||||||
|
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
class Event(
|
class Event(
|
||||||
var title: String = "INVALID",
|
var title: String = "INVALID",
|
||||||
var description: String = "INVALID",
|
var description: String = "INVALID",
|
||||||
|
var start: LocalDateTime = LocalDateTime.MIN,
|
||||||
|
var end: LocalDateTime = LocalDateTime.MIN,
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
@JoinColumn(name = "project_id")
|
@JoinColumn(name = "project_id")
|
||||||
|
@JsonIgnore
|
||||||
var project: Project? = null,
|
var project: Project? = null,
|
||||||
@ManyToMany(mappedBy = "events")
|
@ManyToMany(mappedBy = "events")
|
||||||
var tags: Set<Tag> = mutableSetOf(),
|
@JsonIgnore
|
||||||
@Id @GeneratedValue var id: Long? = null
|
var tags: MutableSet<Tag> = mutableSetOf(),
|
||||||
|
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@ -59,12 +64,14 @@ class Comment(
|
|||||||
var user: User? = null,
|
var user: User? = null,
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
@JoinColumn(name = "event_id")
|
@JoinColumn(name = "event_id")
|
||||||
|
@JsonIgnore
|
||||||
var event: Event? = null,
|
var event: Event? = null,
|
||||||
@ManyToMany(mappedBy = "comments")
|
@ManyToMany(mappedBy = "comments")
|
||||||
var tags: Set<Tag> = mutableSetOf(),
|
@JsonIgnore
|
||||||
|
var tags: MutableSet<Tag> = mutableSetOf(),
|
||||||
var created: LocalDateTime = LocalDateTime.MIN,
|
var created: LocalDateTime = LocalDateTime.MIN,
|
||||||
var comment: String = "INVALID",
|
var comment: String = "INVALID",
|
||||||
@Id @GeneratedValue var id: Long? = null
|
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@ -76,13 +83,15 @@ class Tag(
|
|||||||
joinColumns = [JoinColumn(name = "tag_id", referencedColumnName = "id")],
|
joinColumns = [JoinColumn(name = "tag_id", referencedColumnName = "id")],
|
||||||
inverseJoinColumns = [JoinColumn(name = "comment_id", referencedColumnName = "id")]
|
inverseJoinColumns = [JoinColumn(name = "comment_id", referencedColumnName = "id")]
|
||||||
)
|
)
|
||||||
var comments: Set<Comment> = mutableSetOf(),
|
@JsonIgnore
|
||||||
|
var comments: MutableSet<Comment> = mutableSetOf(),
|
||||||
@ManyToMany
|
@ManyToMany
|
||||||
@JoinTable(
|
@JoinTable(
|
||||||
name = "event_tags",
|
name = "event_tags",
|
||||||
joinColumns = [JoinColumn(name = "tag_id", referencedColumnName = "id")],
|
joinColumns = [JoinColumn(name = "tag_id", referencedColumnName = "id")],
|
||||||
inverseJoinColumns = [JoinColumn(name = "event_id", referencedColumnName = "id")]
|
inverseJoinColumns = [JoinColumn(name = "event_id", referencedColumnName = "id")]
|
||||||
)
|
)
|
||||||
var events: Set<Event> = mutableSetOf(),
|
@JsonIgnore
|
||||||
@Id @GeneratedValue var id: Long? = null
|
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.stereotype.Controller
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
import org.springframework.web.bind.annotation.ResponseBody
|
|
||||||
import org.springframework.ui.Model
|
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
|
@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")
|
@GetMapping("/login")
|
||||||
fun login(model: Model, error: String?, logout: String?): String? {
|
fun login(model: Model, error: String?, logout: String?): String? {
|
||||||
@ -22,24 +45,96 @@ class HtmlController @Autowired constructor(val userRepository: UserRepository,
|
|||||||
return "login"
|
return "login"
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/user/{id}")
|
@GetMapping("/profile")
|
||||||
@ResponseBody
|
fun getLoggedInUser(@AuthenticationPrincipal userDetails: CustomUserDetails, model: Model) : String {
|
||||||
fun getUser(@PathVariable id: Long) : User {
|
val user = DisplayUser(userDetails.user.id!!, userDetails.user.username, userDetails.user.email, "", "")
|
||||||
return userRepository.findById(id).get()
|
model.addAttribute("userData", user)
|
||||||
|
return "profile"
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasPermission(#projectID, 'Long', '')")
|
@PostMapping("/profile")
|
||||||
@GetMapping("/project/{projectID}")
|
@Transactional
|
||||||
@ResponseBody
|
fun updateLoggedInUser(@ModelAttribute userData: DisplayUser, @AuthenticationPrincipal userDetails: CustomUserDetails, model: Model) : String {
|
||||||
fun getProject(@PathVariable projectID: Long) : Project{
|
if(userData.id == userDetails.user.id!! && passwordEncoder().matches(userData.oldPassword, userDetails.password)) {
|
||||||
return projectRepository.findById(projectID).get()
|
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")
|
@PostMapping("/newproject")
|
||||||
@ResponseBody
|
fun createNewProject(@RequestBody newProject: NewProject){
|
||||||
fun getLoggedInUser(@AuthenticationPrincipal userDetails: CustomUserDetails) : Long {
|
val project = Project(title = newProject.name)
|
||||||
return userDetails.user.id!!
|
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>{
|
interface UserRepository : CrudRepository<User, Long>{
|
||||||
fun findByUsername(username: String): User?
|
fun findByUsername(username: String): User?
|
||||||
|
fun findByUsernameIsLike(partialUsername: String) : List<User>
|
||||||
|
fun findByIdNotIn(ids: List<Long>) : List<User>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectRepository : CrudRepository<Project, Long>{}
|
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
|
@Configuration
|
||||||
class SecurityConfig @Autowired constructor(val userDetailsService: UserDetailsServiceImpl) : WebSecurityConfigurerAdapter() {
|
class SecurityConfig @Autowired constructor(val userDetailsService: UserDetailsServiceImpl) : WebSecurityConfigurerAdapter() {
|
||||||
override fun configure(http: HttpSecurity) {
|
override fun configure(http: HttpSecurity) {
|
||||||
http.authorizeRequests().antMatchers("/").permitAll()
|
http.authorizeRequests().antMatchers("/", "/*.jpg", "/*.svg", "/register").permitAll()
|
||||||
.anyRequest().authenticated().and()
|
.anyRequest().authenticated().and()
|
||||||
.formLogin().loginPage("/login").permitAll().and()
|
.formLogin().loginPage("/login").permitAll().and()
|
||||||
.logout().permitAll().and()
|
.logout().logoutSuccessUrl("/").permitAll().and()
|
||||||
.httpBasic()
|
.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>
|
<!DOCTYPE HTML>
|
||||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||||
<head th:fragment="baseHeader(title)">
|
<head th:fragment="baseHeader(title)">
|
||||||
|
<meta charset="UTF-8">
|
||||||
<title th:replace="${title}">Base Title</title>
|
<title th:replace="${title}">Base Title</title>
|
||||||
|
|
||||||
<!-- Common styles and scripts -->
|
<!-- 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 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}">
|
<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>
|
<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>
|
</head>
|
||||||
<body>
|
<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>
|
</body>
|
||||||
</html>
|
</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>
|
<body>
|
||||||
<div class="container">
|
<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}">
|
<form class="form-sign_in" method="POST" th:action="@{/login}">
|
||||||
<h2 class="form-heading">Log in</h2>
|
<div class="form-group mt-3">
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<span th:text="${message}"></span>
|
<span th:text="${message}"></span>
|
||||||
<label for="username">Username:</label>
|
<div class="form-floating mb-3">
|
||||||
<input id="username" name="username" type="text" class="form-control" placeholder="Username" autofocus/>
|
<input type="text" class="form-control form-control-lg" id="username" name="username" autofocus>
|
||||||
|
<label for="username">Username</label>
|
||||||
<label for="password">Password:</label>
|
</div>
|
||||||
<input id="password" name="password" type="password" class="form-control" placeholder="Password"/>
|
<div class="form-floating mb-3">
|
||||||
<span class="has-error" th:text="${error}"></span>
|
<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}"/>
|
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
|
||||||
|
|
||||||
|
<div>
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Log In</button>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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