Spring Boot TaskApp for Beginners with SQL Database, Bootstrap and Heroku

In this post you’ll learn how to build a TaskApp using Spring Boot, a mySQL Database + JPA, and Thymeleaf + Bootstrap as Frontend. Learn how to create, read, update and delete a domain model (Tasks) by building a RESTful CRUD API. You will also learn how to deploy your App to Heroku and how to configure your App to start with preloaded data.

Find a working version of the finished App on Heroku(it may take a while to load) and the Source Code here. Feel free to use the code on Github for practice. The Frontend was designed by Bootstrap and can be found here. Provided under MIT License.

finished Project deployed on Heroku

Development Environment:

IDE: IntelliJ, Eclipse
Install: MySQL Workbench (How to install the mySQL Workbench)
Deployment: Get a free Heroku Account
Java: Install Java on your computer and follow the installation instructions
Version Control: Install Git on your computer.

Import Project to your IDE:

Start by downloading and unpacking the Starter Project from GitHub and import it into an IDE of your choice. This post will feature IntelliJ. (1) Click on Import Project, (2) Select the folder, (3)Click next until finish. Wait until IntelliJ opens and finishes processing.

Import TaskApp Starter Code from GitHub to IntelliJ
Folder Structure: Left – starting code, Right – finished structure for reference
folder structure at beginning and end of post.

pom.xml: SQL, JPA, Thymeleaf, Web and PostgreSQL for Heroku Dependencies:

We will store our data in an SQL Database and later use PostgreSQL to deploy the app to Heroku. PostgreSQL is free on Heroku and will be fine for our purposes. Thymeleaf is a Java Template Engine which we’ll use to code our HTML Frontend.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>taskapp</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>taskapp</name>
    <description>TaskApp with Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- for Heroku deploy-->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.0</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>2.22.0</version>
            </plugin>
        </plugins>
    </build>

</project>

application.properties:

Connects our app to a Database “taskapp_db”. The default port is 8080. With the current cofigurations the app will insert the data from data.sql(below) and create a new DB after every restart of the app (it will drop the old DB if it exists).

spring.datasource.url=jdbc:mysql://localhost/taskapp_db?useSSL=false

#place your password and username from your MySQL Workbench
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.show-sql=true
#init data after every restart
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto=create
spring.datasource.initialization-mode=always

## Hibernate Logging
logging.level.org.hibernate.SQL= DEBUG
INSERT INTO tasks (task_id, content, status, title) VALUES
  ( 1000, '(1)Generate a new project with Spring Initializr using Thymeleaf, MySQl, Web, and JPA', 'CLOSED', 'Setup a new project'),
  ( 1001, '(2)Create Folders for your model, repositories, controllers and services', 'REOPENED', 'Folder Structure'),
  ( 1002, '(3)Write a Task POJO with the following properties: title, status of type Status(Enum), content', 'OPEN', 'Task Entity'),
  ( 1003, '(4)Write an Enum Class for Status containing OPEN, CLOSED, REOPENED', 'OPEN', 'Status Enum'),
  ( 1004, '(5)Create a Service Interface and its Implementation', 'OPEN', 'Service Layer'),
  ( 1005, '(6)Complete the Application.properties file and create a new schema in your sql workbench', 'CLOSED', 'Databasa and Application Properties'),
  ( 1006, '(7)Write a Crud Repository for your Task entity', 'OPEN', 'Repository'),
  ( 1007, '(8)Write a TaskController', 'REOPENED', 'Controller');

 

To run the App:
(1) create a new Schema “taskapp_db” in your mySQL Workbench
(2) change password and username in application.properties file
(3) open the data.sql file. You will be asked to “Configure data source“. Create a new data source as described in the picture below. Enter your password and username from your mySQL Workbench and the name of the Databse (taskapp_db). Be sure to test the connection before closing the window.

(4) run the app by typing “mvn spring-boot:run” in the terminal


(5)open in browser http://localhost:8080 . You should see a blank page with “Congrats! Your App is Running!

UMl Diagram of finished App:

UML Diagramm of the finished Project

Task Model:

The starting code contains a model “Task”. It has a unique Id and a title, content and status. Neither of them may be empty when sending the form information. However this post will not treat validation or exception handling.

@Entity
@Table(name = "tasks")
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "task_id")
    private Long id;

    @NotEmpty
    private String title;

    @Lob
    @NotEmpty
    @Type(type = "org.hibernate.type.TextType") //heroku config
    private String content;

    //status may not be null
    @NotNull
    @Enumerated(value = EnumType.STRING)
    private Status status;

    public Task() {
        this.status = Status.OPEN;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public Status getStatus() {
        return status;
    }

    public void setStatus(Status status) {
        this.status = status;
    }

    public void closeTask(){
        this.status = Status.CLOSED;
    }

    public void reopenTask(){
        this.status = Status.REOPENED;
    }
}

Status:

Every Task will have a status of “OPEN”, “REOPENED” or “CLOSED”.

public enum Status {
    OPEN("open"),
    CLOSED("closed"),
    REOPENED("reopened");

    private String typeOfStatus;

    Status(String typeOfStatus){
        this.typeOfStatus = typeOfStatus;
    }

    public String getTypeOfStatus() {
        return typeOfStatus;
    }

    public static Stream<Status> stream() {
        return Stream.of(Status.values());
    }
}

Repository:

Next, create a new folder “repository” and a new interface “TaskRepository.java”. The repository to save files in the database and retrieve them.

@Repository
public interface TaskRepository extends CrudRepository<Task, Long>{
}

 

Service Layer:

Create a new folder “service” and a new interface “TaskService.java” which will be implemented by “TaskServiceImpl.java”. The TaskService will provide the necessary CRUDfunctions to interact with the DB.

@Service
public interface TaskService {
    /**
     * GET all tasks from DB
     * @return all tasks from Database
     */
    Set<Task> getTasks();

    /**
     * finds a task by its ID
     * @param taskId    Database ID of task
     * @return          task
     */
    Task findById(Long taskId);

    /**
     * creates new Task and saves it in Database
     * @param taskDetails   field values
     * @return              new Task
     */
    Task createTask(Task taskDetails);

    /**
     * updates Task from Database with field values in taskDetails
     * @param taskId        Database ID of task
     * @param taskDetails   field values
     * @return              updated Task
     */
    Task updateTask(Long taskId, Task taskDetails);

    /**
     * deletes Task from Database
     * @param taskId    Database ID of task
     */
    void deleteTask(Long taskId);

    /**
     * Sets Task.Status to CLOSED
     * @param taskId    Database ID of task
     */
    void closeTask(Long taskId);

    /**
     * Sets Task.Status to REOPENED
     * @param taskId    Database ID of task
     */
    void reopenTask(Long taskId);

    /**
     * Filters all Tasks by Status
     * @param status    Enum: OPEN;CLOSED;REOPENED
     * @return          Set of Tasks
     */
    Set<Task> getTasksByStatus(Status status);

}

TaskServiceImpl.Java:

@Service
public class TaskServiceImpl implements TaskService{

    @Autowired
    private TaskRepository taskRepository;

    @Override
    public Set<Task> getTasks() {
        Set<Task> taskSet = new HashSet<>();
        taskRepository.findAll().iterator().forEachRemaining(taskSet::add);
        return taskSet;
    }

    @Override
    public Task findById(Long taskId) {
        Optional<Task> taskOptional = taskRepository.findById(taskId);
        if(!taskOptional.isPresent()){
            throw new RuntimeException("Task Not Found!");
        }
        return taskOptional.get();
    }

  
    @Override
    public Task createTask(Task taskDetails) {
        Task newTask = taskRepository.save(taskDetails);
        return newTask;
    }

  
    @Override
    public Task updateTask(Long taskId, Task taskDetails) {
        Task task = findById(taskId);
        task.setTitle(taskDetails.getTitle());
        task.setContent(taskDetails.getContent());
        task.setStatus(taskDetails.getStatus());
        taskRepository.save(task);
        return task;
    }

  
    @Override
    public void deleteTask(Long taskId) {
        taskRepository.delete(findById(taskId));
    }

 
    @Override
    public void closeTask(Long taskId) {
        Task task = findById(taskId);
        task.closeTask();
        taskRepository.save(task);
    }

   
    @Override
    public void reopenTask(Long taskId) {
        Task task = findById(taskId);
        task.reopenTask();
        taskRepository.save(task);
    }

 
    @Override
    public Set<Task> getTasksByStatus(Status status) {
        Set<Task> allTasks = getTasks();
        Set<Task> filteredTasks = new HashSet<>();

        for(Task t : allTasks){
            if(t.getStatus().equals(status)){filteredTasks.add(t);}
        }

        return filteredTasks;
    }
}

 

Controller:

The following will be the controller for our Task Entity.

@Controller
public class TaskController {

    @Autowired
    private TaskService taskService;

    /**
     * GET all tasks from Database
     * @return template view for all tasks
     */
    @RequestMapping(value = {"/tasks", "/"}, method=RequestMethod.GET)
    public String dashboard(Model model) {
        //display all Tasks
        Set<Task> tasks = taskService.getTasks();
        model.addAttribute("tasks", tasks);
        //newTask Form
        Task newTask = new Task();
        model.addAttribute("newTask", newTask);

        Set<Status> statusList = new HashSet<>();
        Status.stream().forEach(statusList::add);
        model.addAttribute("statusList", statusList);

        return "index";
    }

    /**
     * Shows Tasks by Status
     * @param model         contains TaskObject
     * @param taskStatus    may have the values "open/closed/reopened"
     * @return              Set of Tasks with specific status
     */
    @RequestMapping(value = "/{status}", method=RequestMethod.GET)
    public String displayByStatus(Model model, @PathVariable("status") String taskStatus) {

        if(taskStatus.equals(Status.OPEN.getTypeOfStatus())){
            model.addAttribute("tasks", taskService.getTasksByStatus(Status.OPEN));
        } else if(taskStatus.equals(Status.CLOSED.getTypeOfStatus())){
            model.addAttribute("tasks", taskService.getTasksByStatus(Status.CLOSED));
        } else if(taskStatus.equals(Status.REOPENED.getTypeOfStatus())){
            model.addAttribute("tasks", taskService.getTasksByStatus(Status.REOPENED));
        }

        //for newTask Form
        Task newTask = new Task();
        model.addAttribute("newTask", newTask);

        Set<Status> statusList = new HashSet<>();
        Status.stream().forEach(statusList::add);
        model.addAttribute("statusList", statusList);

        return "index";
    }

    /**
     * handles Status Changes
     * @param taskId        Task Id
     * @param action        may contain "close/open/reopen"
     * @param request       helps redirect to previous site
     * @return              redirection
     */
    @RequestMapping(value = "/task/{id}/{action}", method=RequestMethod.GET)
    public String handleStatus(@PathVariable("id") Long taskId,
                               @PathVariable("action") String action,
                               HttpServletRequest request){
        Task task = taskService.findById(taskId);

        if (action.equals("close")) {
            if(task.getStatus() == Status.OPEN) {taskService.closeTask(taskId);}
            if(task.getStatus() == Status.REOPENED) {taskService.closeTask(taskId);}
        }
        if(action.equals("reopen") && task.getStatus() == Status.CLOSED) {taskService.reopenTask(taskId);}

        String referer = request.getHeader("Referer");
        return "redirect:"+ referer;
    }

    /**
     * Save NEW Task in Database
     * @param taskDetails   field values
     * @return              redirect to Dashboard
     */
    @RequestMapping(path = "/task/create", method = RequestMethod.POST)
    public String createTask(Task taskDetails) {
        Task newTask = taskService.createTask(taskDetails);
        return "redirect:/";
    }

    /**
     * Displays an EDIT Form for a Task
     * @param model         task Object
     * @param taskId        Id of the Task
     * @return              edit Form
     */
    @RequestMapping(value = "/task/{id}/edit", method=RequestMethod.GET)
    public String editForm(Model model, @PathVariable("id") Long taskId) {
        Set<Status> statusList = new HashSet<>();
        Status.stream().forEach(statusList::add);
        model.addAttribute("statusList", statusList);
        model.addAttribute("editTask", taskService.findById(taskId));
        return "editView";
    }

    /**
     * Update Task and save changes in Database
     * @param taskId        TaskId
     * @param taskDetails   field values
     * @return              redirect to Dashboard
     */
    @RequestMapping(path = "/task/{id}/update", method = RequestMethod.POST)
    public String updateTask(@PathVariable("id") long taskId, Task taskDetails) {
        taskService.updateTask(taskId, taskDetails);
        return "redirect:/";
    }

    /**
     * Deletes Task from Database
     * @param taskId    TaskId
     * @return          redirect to Dashboard
     */
    @RequestMapping(path = "/task/{id}/delete", method = RequestMethod.GET)
    public String deleteTask(@PathVariable("id") long taskId, HttpServletRequest request) {
        taskService.deleteTask(taskId);
        return "redirect:/";
    }

}

Now the Backend is completed. For the Frontend create two new folders “fragments” and “tasks” in the templates folder. The fragments folder will contain snippets like the head and footer of the index.html. In tasks we’ll create snippets for editing files and creating new Tasks.

index.html

Copy the following into the index.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<th:block th:include="fragments/header"></th:block>

<body id="page-top">

  <!-- Page Wrapper -->
  <div id="wrapper">

    <th:block th:include="fragments/sidebarLeft"></th:block>

    <!-- Content Wrapper -->
    <div id="content-wrapper" class="d-flex flex-column">

      <!-- Main Content -->
      <div id="content">

        <th:block th:include="fragments/navbar"></th:block>

        <!-- Begin Page Content -->
        <div class="container-fluid">

          <!-- Content Row -->
          <div class="row">
            <!-- List of Tasks-->
            <th:block th:include="tasks/tasks"></th:block>

            <!-- New Task -->
            <th:block th:include="tasks/newTask"></th:block>

          </div>

        </div>
        <!-- /.container-fluid -->

      </div>
      <!-- End of Main Content -->

      <th:block th:include="fragments/footer"></th:block>

    </div>
    <!-- End of Content Wrapper -->

  </div>
  <!-- End of Page Wrapper -->

  <!-- Scroll to Top Button-->
  <a class="scroll-to-top rounded" href="#page-top">
    <i class="fas fa-angle-up"></i>
  </a>

  <th:block th:include="fragments/scripts"></th:block>

</body>

</html>

In the “tasks” folder create the following files:

tasks.html:

<div class="col-xl-8 col-lg-7">

    <div th:if="${#lists.isEmpty(tasks)}" class="card shadow mb-4">
        <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
            <h6 class="m-0 font-weight-bold text-primary">&nbsp;&nbsp;</h6>
        </div>
        <!-- Card Body -->
        <div class="card-body">
            <p>Your Task List is empty.</p>
        </div>
    </div>

    <div th:each = "task : ${tasks}" class="card shadow mb-4">
        <!-- Card Header - Dropdown -->
        <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">

            <!-- Info Button for OPEN Tasks-->
            <div th:if="${task.status == T(com.example.taskapp.model.Status).OPEN}" class="btn btn-primary btn-icon-split">
                    <span class="icon text-white-50">
                      OPEN
                    </span>
                <span class="text" th:text="${task.title}"></span>
            </div>

            <!-- Info Button for CLOSED Tasks-->
            <div th:if="${task.status == T(com.example.taskapp.model.Status).CLOSED}" class="btn btn-success btn-icon-split">
                    <span class="icon text-white-50">
                      CLOSED
                    </span>
                <span class="text" th:text="${task.title}"></span>
            </div>

            <!-- Info Button for REOPENED Tasks-->
            <div th:if="${task.status == T(com.example.taskapp.model.Status).REOPENED}" class="btn btn-warning btn-icon-split">
                    <span class="icon text-white-50">
                      REOPENED
                    </span>
                <span class="text" th:text="${task.title}"></span>
            </div>

            <h6 class="m-0 font-weight-bold text-primary">&nbsp;&nbsp;</h6>
            <!-- Dropdown -->
            <div class="dropdown no-arrow">
                <a class="dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                    <i class="fas fa-ellipsis-v fa-sm fa-fw text-gray-400"></i>
                </a>
                <div class="dropdown-menu dropdown-menu-right shadow animated--fade-in" aria-labelledby="dropdownMenuLink">
                    <div class="dropdown-header">Actions:</div>

                    <div th:if="${task.status == T(com.example.taskapp.model.Status).OPEN}">
                        <a class="dropdown-item" th:href="@{/task/{id}/{action}(id=${task.id}, action='close')}">CLOSE</a>
                    </div>
                    <div th:if="${task.status == T(com.example.taskapp.model.Status).CLOSED}">
                        <a class="dropdown-item" th:href="@{/task/{id}/{action}(id=${task.id}, action='reopen')}">REOPEN</a>
                    </div>
                    <div th:if="${task.status == T(com.example.taskapp.model.Status).REOPENED}">
                        <a class="dropdown-item" th:href="@{/task/{id}/{action}(id=${task.id}, action='close')}">CLOSE</a>
                    </div>

                    <div class="dropdown-divider"></div>
                    <a class="dropdown-item" th:href="@{/task/{id}/edit(id=${task.id})}"><i class="far fa-edit"></i> Edit</a>
                    <a class="dropdown-item" th:href="@{/task/{id}/delete(id=${task.id})}"><i class="fas fa-trash-alt"></i> Delete</a>
                </div>
            </div>
        </div>
        <!-- Card Body -->
        <div class="card-body">
            <p th:text="${task.content}"></p>
        </div>
    </div>
</div>

newTask.html:

<div class="col-xl-4 col-lg-5">
    <div class="card shadow mb-4">
        <!-- Card Header - Dropdown -->
        <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
            <h6 class="m-0 font-weight-bold text-primary">New Task</h6>
        </div>
        <!-- Card Body -->
        <div class="card-body">
            <form th:object="${newTask}" th:action="@{/task/create}" th:method="post">
                <div class="form-group">
                    <label for="title">Title:</label>
                    <input th:field="*{title}" type="text" class="form-control" id="title">
                </div>
                <div class="form-group">
                    <label for="status">Status:</label>
                    <select id="status" class="form-control" th:field="*{status}">
                        <option th:each="status : ${statusList}" th:text="${status}" th:value="${status}"></option>
                    </select>
                </div>
                <div class="form-group">
                    <label for="taskContent">Content:</label>
                    <textarea th:field="*{content}" class="form-control" id="taskContent" rows="5"></textarea>
                </div>
                <button type="submit" class="btn btn-primary">Save</button>
            </form>
        </div>
    </div>
</div>

editTask.html:

<div class="col-xl-8 col-lg-7">

    <div class="card shadow mb-4">
        <!-- Card Header - Dropdown -->
        <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
            <h6 class="m-0 font-weight-bold text-primary">Edit Task</h6>
        </div>
        <!-- Card Body -->
        <div class="card-body">
            <form th:object="${editTask}" th:action="@{/task/{id}/update(id=${editTask.id})}" th:method="post">
                <div class="form-group">
                    <label for="title">Title:</label>
                    <input th:field="*{title}" type="text" class="form-control" id="title">
                </div>
                <div class="form-group">
                    <label for="status">Status:</label>
                    <select id="status" class="form-control" th:field="*{status}">
                        <option th:each="status : ${statusList}" th:text="${status}" th:value="${status}"></option>
                    </select>
                </div>
                <div class="form-group">
                    <label for="taskContent">Content:</label>
                    <textarea th:field="*{content}" class="form-control" id="taskContent" rows="5"></textarea>
                </div>
                <button type="submit" class="btn btn-primary">Save</button>
                <a th:href="@{/task/{id}/delete(id=${editTask.id})}" class="btn btn-danger">Delete</a>
            </form>
        </div>
    </div>

</div>

Next, in the fragments folder:

header.html:

<head>

    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>TaskApp</title>

    <!-- Custom fonts for this template-->
    <link th:href="@{/vendor/fontawesome-free/css/all.min.css}" rel="stylesheet" type="text/css">
    <link th:href="@{https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i}" rel="stylesheet">

    <!-- Custom styles for this template-->
    <link th:href="@{/css/sb-admin-2.min.css}" rel="stylesheet">

</head>

navbar.html:

<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
    <!-- Sidebar Toggle (Topbar) -->
    <button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
        <i class="fa fa-bars"></i>
    </button>

    <!-- Topbar Navbar -->
    <ul class="navbar-nav ml-auto"></ul>

</nav>

sidebarLeft.html:

<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">

    <!-- Sidebar - Brand -->
    <a class="sidebar-brand d-flex align-items-center justify-content-center" th:href="@{/}">
        <div class="sidebar-brand-text mx-3">TaskApp</div>
    </a>

    <!-- Divider -->
    <hr class="sidebar-divider my-0">

    <!-- Nav Item - Dashboard -->
    <li class="nav-item">
        <a class="nav-link" th:href="@{/}">
            <i class="fas fa-fw fa-tachometer-alt"></i>
            <span>Dashboard</span></a>
    </li>

    <!-- Divider -->
    <hr class="sidebar-divider">


    <!-- Heading -->
    <div class="sidebar-heading">
        Tasks by Status
    </div>

    <!-- Nav Item - Charts -->
    <li class="nav-item">
        <a class="nav-link" th:href="@{/{status}(status='open')}">
            <i class="fas fa-fw fa-chart-area"></i>
            <span>OPEN</span></a>
    </li>

    <!-- Nav Item - Charts -->
    <li class="nav-item">
        <a class="nav-link" th:href="@{/{status}(status='reopened')}">
            <i class="fas fa-fw fa-chart-area"></i>
            <span>REOPENED</span></a>
    </li>

    <!-- Nav Item - Tables -->
    <li class="nav-item">
        <a class="nav-link" th:href="@{/{status}(status='closed')}">
            <i class="fas fa-fw fa-table"></i>
            <span>CLOSED</span></a>
    </li>

    <!-- Divider -->
    <hr class="sidebar-divider d-none d-md-block">

    <!-- Sidebar Toggler (Sidebar) -->
    <div class="text-center d-none d-md-inline">
        <button class="rounded-circle border-0" id="sidebarToggle"></button>
    </div>

</ul>

footer.html:

<footer class="sticky-footer bg-white">
    <div class="container my-auto">
        <div class="copyright text-center my-auto">
            <span>Copyright &copy; <script>document.write(new Date().getFullYear())</script> platoiscoding.com</span>
        </div>
        <div class="copyright pull-right">

        </div>
    </div>
</footer>

scripts.html:

<!-- Bootstrap core JavaScript-->
<script th:src="@{/vendor/jquery/jquery.min.js}"></script>
<script th:src="@{/vendor/bootstrap/js/bootstrap.bundle.min.js}"></script>

<!-- Core plugin JavaScript-->
<script th:src="@{/vendor/jquery-easing/jquery.easing.min.js}"></script>

<!-- Custom scripts for all pages-->
<script th:src="@{/js/sb-admin-2.min.js}"></script>

At last, create the file “editView.html” in the same directory as index.html. It will contain the view for editing Tasks.

editView.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<th:block th:include="fragments/header"></th:block>

<body id="page-top">

<!-- Page Wrapper -->
<div id="wrapper">

    <th:block th:include="fragments/sidebarLeft"></th:block>

    <!-- Content Wrapper -->
    <div id="content-wrapper" class="d-flex flex-column">

        <!-- Main Content -->
        <div id="content">

            <th:block th:include="fragments/navbar"></th:block>

            <!-- Begin Page Content -->
            <div class="container-fluid">

                <!-- Content Row -->
                <div class="row">
                    <!-- edit Task-->
                    <th:block th:include="tasks/editTask"></th:block>

                </div>

            </div>
            <!-- /.container-fluid -->

        </div>
        <!-- End of Main Content -->

        <th:block th:include="fragments/footer"></th:block>

    </div>
    <!-- End of Content Wrapper -->

</div>
<!-- End of Page Wrapper -->

<!-- Scroll to Top Button-->
<a class="scroll-to-top rounded" href="#page-top">
    <i class="fas fa-angle-up"></i>
</a>

<th:block th:include="fragments/scripts"></th:block>

</body>

</html>

Type the following command to run the application: mvn spring-boot:run

 

Deploy your App to Heroku:

application.properties.file: Delete all and place the following

## Heroku Properties
spring.datasource.url=${JDBC_DATABSE_URL}
spring.datasource.username=${JDBC_DATABSE_USERNAME}
spring.datasource.password=${JDBC_DATABSE_PASSWORD}

spring.jpa.show-sql = false
spring.jpa.generate.ddl = true
spring.jpa.hibernate.ddl-auto = create
        
spring.datasource.initialization-mode=always

Next open the command line runner and navigate to your application folder

(1) enter: “heroku login” and provide your email and password (you need to have an account on Heroku by now)
(2) then: “git init” and “git add .” which will add all your files to the version control
(3) git commit -m “your commit message”
(4) heroku create “name of your app”
(5) git push heroku master
(6) heroku open – will open your deployed app in your browser

Finished. Thanks for reading. This is my first tutorial, so if you run into problems please let me know.

Leave a Reply