Implement user creation, started project viewing

This commit is contained in:
neviyn 2021-03-30 18:32:24 +01:00
parent 1925220823
commit da45f0c633
15 changed files with 514 additions and 63 deletions

View File

@ -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>

View File

@ -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
) )

View File

@ -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)
}
}

View File

@ -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>{}

View 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)

View File

@ -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()
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

View 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>

View File

@ -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>

View 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>

View File

@ -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>

View 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>

View 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>

View 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>

View 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>