SpringBoot

Java SpringBoot MySQL Blog Project – Back-end Front-end Both

📝 SpringBoot Blog Project – Introduction

The SpringBoot Blog Project is a dynamic, full-stack web application designed to facilitate user engagement through rich content creation, publication, and management. Built with a robust Java-based backend using the Spring Boot framework, this blog system delivers high performance and modular scalability. It includes both an admin interface for managing posts, categories, and comments, and a frontend interface for public readers to browse articles with aesthetic layout and responsiveness.The project is integrated with MySQL for persistent storage of posts, users, and categories, and uses Thymeleaf as the template engine for seamless rendering of dynamic HTML content. On the frontend, HTML5, CSS3, and JavaScript ensure a modern and user-friendly UI with responsive design. This architecture is ideal for learning and implementing MVC (Model-View-Controller) concepts, layered architecture, RESTful APIs, and secure form handling.In summary, this blog system serves as a perfect foundation for developers looking to build real-world web applications using Spring Boot and modern web technologies. It’s ideal for students, hobbyists, and professionals aiming to showcase their full-stack Java development skills.

🧱 Project Folder Structure & Technology Stack
✅ Technologies Used
Frontend: HTML5, CSS3, JavaScript

Backend: Java, Spring Boot, Thymeleaf

Database: MySQL

Template Engine: Thymeleaf

 

company name :  acesoftech

Project Name: Blog

 

📁 Project Structure Overview

E:\JAVA\SPRINGBOOT\BLOG\SRC
├───main
│   ├───java
│   │   └───com
│   │       └───acesoftech
│   │           └───blog
│   │               │   SpringbootBlogApplication.java
│   │               │
│   │               ├───config
│   │               │       SecurityConfig.java
│   │               │
│   │               ├───controller
│   │               │   ├───admin
│   │               │   │       AdminCategoryController.java
│   │               │   │       AdminCommentController.java
│   │               │   │       AdminDashboardController.java
│   │               │   │       AdminLoginController.java
│   │               │   │       AdminPostController.java
│   │               │   │
│   │               │   └───frontend
│   │               │           FrontendArticleController.java
│   │               │           FrontendCategoryController.java
│   │               │           FrontendHomeController.java
│   │               │           FrontendInfoController.java
│   │               │
│   │               ├───dto
│   │               │       CategoryDTO.java
│   │               │       PostDTO.java
│   │               │
│   │               ├───entity
│   │               │       Category.java
│   │               │       Comment.java
│   │               │       Post.java
│   │               │       User.java
│   │               │
│   │               ├───repository
│   │               │       CategoryRepository.java
│   │               │       CommentRepository.java
│   │               │       PostRepository.java
│   │               │       UserRepository.java
│   │               │
│   │               └───service
│   │                   └───impl
│   └───resources
│       │   application.properties
│       │   application.yml
│       │
│       ├───static 
│       │   │
│       │   └───uploads
│       │           2bf1b56c-1a69-439a-8242-b7da38848a1a_f4.png
│       │
│       └───templates
│           ├───admin
│           │   │   categories.html
│           │   │   category-form.html
│           │   │   comments.html
│           │   │   dashboard.html
│           │   │   layout.html
│           │   │   login.html
│           │   │   post-form.html
│           │   │   posts.html
│           │   │
│           │   └───fragments
│           │           footer.html
│           │           header.html
│           │
│           └───front
│               │   about.html
│               │   contact.html
│               │   index.html
│               │   layout.html
│               │   post-list-by-category.html
│               │   post-view.html
│               │   services.html
│               │
│               └───fragments
│                       footer.html
│                       header.html
│                       sidebar.html
│
└───test
    └───java
        └───com
            └───acesoftech
                └───blog
                        BlogApplicationTests.java

application.properties

spring.application.name=acesoftech-blog
server.port=8080

# Database Configuration
spring.datasource.url=jdbc:mysql://localhost:3308/blogdb
spring.datasource.username=myadmin
spring.datasource.password=Angular13
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# File Upload Limits
spring.servlet.multipart.max-file-size=64MB
spring.servlet.multipart.max-request-size=64MB

# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# Spring Security Default User (for basic auth)
spring.security.user.name=admin
spring.security.user.password=admin123

 

🧱 pom.xml

<?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 https://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>3.2.5</version> <!-- ✅ stable version -->
    <relativePath/> <!-- lookup parent from repository -->
</parent>

    <groupId>com.acesoftech</groupId>
    <artifactId>blog</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>blog</name>
    <description>Demo project for Spring Boot</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>17</java.version>
    </properties>



    <dependencies>


<dependency>

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


<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-core</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
 
  <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </pluginRepository>
    </pluginRepositories>

</project>

 

// src/main/java/com/acesoftech/blog/SpringbootBlogApplication.java

package com.acesoftech.blog;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringbootBlogApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootBlogApplication.class, args);
    }
}

// src/main/java/com/acesoftech/blog/config/SecurityConfig.java

package com.acesoftech.blog.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // Define in-memory user with username and password here
    @Bean
    public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) {
        UserDetails user = User.withUsername("admin")
                .password(passwordEncoder.encode("admin123"))
                .roles("ADMIN")
                .build();

        return new InMemoryUserDetailsManager(user);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").authenticated()
                .anyRequest().permitAll()
            )
            .formLogin(form -> form
                .loginPage("/admin/login")
                .defaultSuccessUrl("/admin/dashboard", true)
                .permitAll()
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/admin/login")
                .permitAll()
            );

        return http.build();
    }
}

 

 


FRONT-END CONTROLLER

// src/main/java/com/acesoftech/blog/controller/admin/AdminCategoryController.java

package com.acesoftech.blog.controller.admin;

import com.acesoftech.blog.entity.Category;
import com.acesoftech.blog.repository.CategoryRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Controller
@RequestMapping("/admin/categories")
public class AdminCategoryController {

    @Autowired
    private CategoryRepository categoryRepository;

    // List all categories
    @GetMapping
    public String getAllCategories(Model model) {
        List<Category> categories = categoryRepository.findAll();
        model.addAttribute("categories", categories);
        return "admin/categories";
    }

    // Show form to add new category
   @GetMapping("/new")
public String showAddForm(Model model) {
    model.addAttribute("category", new Category());
    model.addAttribute("categories", categoryRepository.findAll()); // 👈 Add this
    return "admin/category-form";
}
    // Show form to edit category
   @GetMapping("/edit/{id}")
public String showEditForm(@PathVariable Long id, Model model) {
    Category category = categoryRepository.findById(id)
        .orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + id));
    model.addAttribute("category", category);
    model.addAttribute("categories", categoryRepository.findAll()); // 👈 Add this
    return "admin/category-form";
}

    // Save category (add or update)
    @PostMapping("/save")
    public String saveCategory(@ModelAttribute Category category) {
        categoryRepository.save(category);
        return "redirect:/admin/categories";
    }

    // Delete category
    @GetMapping("/delete/{id}")
    public String deleteCategory(@PathVariable Long id) {
        categoryRepository.deleteById(id);
        return "redirect:/admin/categories";
    }
}

// src/main/java/com/acesoftech/blog/controller/admin/AdminCommentController.java

package com.acesoftech.blog.controller.admin;

import com.acesoftech.blog.entity.Comment;
import com.acesoftech.blog.repository.CommentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Controller
@RequestMapping("/admin/comments")
public class AdminCommentController {

    @Autowired
    private CommentRepository commentRepository;

    // Show list of all comments
    @GetMapping
    public String listComments(Model model) {
        List<Comment> comments = commentRepository.findAll();
        model.addAttribute("comments", comments);
        return "admin/comments";
    }

    // Approve comment (status = 1)
    @GetMapping("/approve/{id}")
    public String approveComment(@PathVariable Long id) {
        Comment comment = commentRepository.findById(id).orElse(null);
        if (comment != null) {
            comment.setStatus(1);
            commentRepository.save(comment);
        }
        return "redirect:/admin/comments";
    }

    // Unapprove comment (status = 0)
    @GetMapping("/unapprove/{id}")
    public String unapproveComment(@PathVariable Long id) {
        Comment comment = commentRepository.findById(id).orElse(null);
        if (comment != null) {
            comment.setStatus(0);
            commentRepository.save(comment);
        }
        return "redirect:/admin/comments";
    }

    // Delete comment
    @GetMapping("/delete/{id}")
    public String deleteComment(@PathVariable Long id) {
        commentRepository.deleteById(id);
        return "redirect:/admin/comments";
    }
}

// src/main/java/com/acesoftech/blog/controller/admin/AdminDashboardController.java

package com.acesoftech.blog.controller.admin;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/admin/dashboard")
public class AdminDashboardController {

    @GetMapping
    public String dashboard(Model model) {
        model.addAttribute("pageTitle", "Admin Dashboard");
        return "admin/dashboard"; // ✅ renders templates/admin/dashboard.html
    }
}

// src/main/java/com/acesoftech/blog/controller/admin/AdminLoginController.java

package com.acesoftech.blog.controller.admin;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/admin")
public class AdminLoginController {

    @GetMapping("/login")
    public String showLoginForm() {
        return "admin/login"; // Points to templates/admin/login.html
    }
}

// src/main/java/com/acesoftech/blog/controller/admin/AdminPostController.java

package com.acesoftech.blog.controller.admin;

import com.acesoftech.blog.entity.Category;
import com.acesoftech.blog.entity.Post;
import com.acesoftech.blog.repository.CategoryRepository;
import com.acesoftech.blog.repository.PostRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;


import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.UUID;


import java.nio.file.Path;


@Controller
@RequestMapping("/admin/posts")
public class AdminPostController {

    @Autowired
    private PostRepository postRepository;

    @Autowired
    private CategoryRepository categoryRepository;

    // ✅ Show all posts
    @GetMapping
    public String getAllPosts(Model model) {
        List<Post> posts = postRepository.findAll();
        model.addAttribute("posts", posts);
        return "admin/posts";
    }

    // ✅ Show form to add new post
    @GetMapping("/new")
    public String showAddForm(Model model) {
        model.addAttribute("post", new Post());
        model.addAttribute("categories", categoryRepository.findAll());
        return "admin/post-form";
    }

    // ✅ Show form to edit existing post
    @GetMapping("/edit/{id}")
    public String showEditForm(@PathVariable Long id, Model model) {
        Post post = postRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Invalid post ID: " + id));
        model.addAttribute("post", post);
        model.addAttribute("categories", categoryRepository.findAll());
        return "admin/post-form";
    }

    // ✅ Save post (create/update)
@PostMapping("/save")
public String savePost(@ModelAttribute Post post,
                       @RequestParam("imageFile") MultipartFile imageFile) throws IOException {

    if (!imageFile.isEmpty()) {
        String fileName = UUID.randomUUID() + "_" + imageFile.getOriginalFilename();
        Path uploadPath = Paths.get("src/main/resources/static/uploads");

        if (!Files.exists(uploadPath)) {
            Files.createDirectories(uploadPath);
        }

        Path filePath = uploadPath.resolve(fileName);
        Files.copy(imageFile.getInputStream(), filePath);

        post.setImageName(fileName);
    }

    postRepository.save(post);
    return "redirect:/admin/posts";
}
    // ✅ Delete post
    @GetMapping("/delete/{id}")
    public String deletePost(@PathVariable Long id) {
        postRepository.deleteById(id);
        return "redirect:/admin/posts";
    }
}

FRONT-END CONTROLELRS

// src/main/java/com/acesoftech/blog/controller/frontend/FrontendArticleController.java

package com.acesoftech.blog.controller.frontend;

import com.acesoftech.blog.entity.Category;
import com.acesoftech.blog.entity.Comment;
import com.acesoftech.blog.entity.Post;
import com.acesoftech.blog.repository.CategoryRepository;
import com.acesoftech.blog.repository.CommentRepository;
import com.acesoftech.blog.repository.PostRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.List;

@Controller
@RequestMapping("/post")
public class FrontendArticleController {

    @Autowired
    private PostRepository postRepository;

    @Autowired
    private CommentRepository commentRepository;

    @Autowired
    private CategoryRepository categoryRepository; // ✅ Inject category repo

    @GetMapping("/{slug}")
    public String viewPost(@PathVariable String slug, Model model) {
        Post post = postRepository.findBySlug(slug);
        if (post == null) {
            return "redirect:/";
        }

        // ✅ Only approved comments
        List<Comment> approvedComments = commentRepository.findByPostAndStatus(post, 1);
        post.setComments(approvedComments);

        // ✅ Get all categories for sidebar
        List<Category> categories = categoryRepository.findAll();

        model.addAttribute("post", post);
        model.addAttribute("categories", categories); // ✅ Add to model
        return "front/post-view";
    }

    @PostMapping("/{slug}/comment")
    public String submitComment(@PathVariable String slug,
                                 @RequestParam Long postId,
                                 @RequestParam String fullName,
                                 @RequestParam String title,
                                 @RequestParam String description) {

        Post post = postRepository.findById(postId).orElse(null);
        if (post == null) {
            return "redirect:/";
        }

        Comment comment = new Comment();
        comment.setFullName(fullName);
        comment.setTitle(title);
        comment.setDescription(description);
        comment.setPost(post);
        comment.setCreatedAt(LocalDateTime.now());
        comment.setStatus(0); // Default to pending

        commentRepository.save(comment);

        return "redirect:/post/" + slug;
    }
}

 

// src/main/java/com/acesoftech/blog/controller/frontend/FrontendCategoryController.java

package com.acesoftech.blog.controller.frontend;

import com.acesoftech.blog.entity.Category;
import com.acesoftech.blog.entity.Post;
import com.acesoftech.blog.repository.CategoryRepository;
import com.acesoftech.blog.repository.PostRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Controller
@RequestMapping("/category")
public class FrontendCategoryController {

    @Autowired
    private CategoryRepository categoryRepository;

    @Autowired
    private PostRepository postRepository;

   @GetMapping("/{id}")
public String viewCategory(@PathVariable Long id, Model model) {
    Category category = categoryRepository.findById(id).orElse(null);
    if (category == null) {
        return "redirect:/"; // fallback if invalid
    }

    List<Post> posts = postRepository.findByCategoryAndActiveTrueOrderByIdDesc(category);
    List<Category> categories = categoryRepository.findAll(); // ✅ ADD THIS

    model.addAttribute("category", category);
    model.addAttribute("posts", posts);
    model.addAttribute("categories", categories); // ✅ ADD THIS

    return "front/post-list-by-category";
}

}

// src/main/java/com/acesoftech/blog/controller/frontend/FrontendHomeController.java

package com.acesoftech.blog.controller.frontend;

import com.acesoftech.blog.entity.Category;
import com.acesoftech.blog.entity.Post;
import com.acesoftech.blog.repository.CategoryRepository;
import com.acesoftech.blog.repository.PostRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

@Controller
public class FrontendHomeController {

    @Autowired
    private PostRepository postRepository;

    @Autowired
    private CategoryRepository categoryRepository;

    @GetMapping("/")
    public String home(Model model) {
        // ✅ Fetch 3 featured posts
        List<Post> featuredPosts = postRepository.findTop3ByFeaturedTrueAndActiveTrueOrderByIdDesc();

        // ✅ Fetch 9 latest posts
        List<Post> latestPosts = postRepository.findTop9ByActiveTrueOrderByIdDesc();

        // ✅ All categories for sidebar
        List<Category> categories = categoryRepository.findAll();

        // ✅ Add to model
        model.addAttribute("featuredPosts", featuredPosts);
        model.addAttribute("posts", latestPosts); // still use 'posts' for latest
        model.addAttribute("categories", categories);

        return "front/index";
    }
}

// src/main/java/com/acesoftech/blog/controller/frontend/FrontendInfoController.java

package com.acesoftech.blog.controller.frontend;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

@Controller
public class FrontendInfoController {

    @GetMapping("/about")
    public String about(Model model) {
        model.addAttribute("pageTitle", "About Us");
        return "front/about"; // loads templates/front/about.html
    }

    @GetMapping("/contact")
    public String contact(Model model) {
        model.addAttribute("pageTitle", "Contact");
        return "front/contact"; // loads templates/front/contact.html
    }

    @GetMapping("/services")
    public String services(Model model) {
        model.addAttribute("pageTitle", "Our Services");
        return "front/services"; // loads templates/front/services.html
    }

    @GetMapping("/search")
    public String search(@RequestParam String keyword, Model model) {
        model.addAttribute("pageTitle", "Search");
        model.addAttribute("keyword", keyword);
        return "front/search"; // loads templates/front/search.html
    }
}

DTO FILES

// src/main/java/com/acesoftech/blog/dto/CategoryDTO.java
// Put content here.

// src/main/java/com/acesoftech/blog/dto/PostDTO.java
// Put content here.

 

ENTITY FILES

// src/main/java/com/acesoftech/blog/entity/Category.java

package com.acesoftech.blog.entity;

import jakarta.persistence.*;
import java.util.List;

@Entity
public class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String slug;
    private String description;
    private Long parentId;
    private boolean active;

    @OneToMany(mappedBy = "category")
    private List<Post> posts;

    // Getter and Setter for id
    public Long getId() {
        return id;
    }

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

    // Getter and Setter for name
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    // Getter and Setter for slug
    public String getSlug() {
        return slug;
    }

    public void setSlug(String slug) {
        this.slug = slug;
    }

    // Getter and Setter for description
    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    // Getter and Setter for parentId
    public Long getParentId() {
        return parentId;
    }

    public void setParentId(Long parentId) {
        this.parentId = parentId;
    }

    // Getter and Setter for active
    public boolean isActive() {
        return active;
    }

    public void setActive(boolean active) {
        this.active = active;
    }

    // Getter and Setter for posts
    public List<Post> getPosts() {
        return posts;
    }

    public void setPosts(List<Post> posts) {
        this.posts = posts;
    }
}

 

// src/main/java/com/acesoftech/blog/entity/Comment.java

package com.acesoftech.blog.entity;

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
public class Comment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String fullName;

    private String title;

    @Column(columnDefinition = "TEXT")
    private String description;

    private LocalDateTime createdAt = LocalDateTime.now();

    // ✅ NEW FIELD: Comment status (0 = off, 1 = approved)
    @Column(nullable = false, columnDefinition = "INT DEFAULT 0")
    private int status = 0;

    @ManyToOne
    @JoinColumn(name = "post_id", nullable = false)
    private Post post;

    // Getters and Setters
    public Long getId() {
        return id;
    }

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

    public String getFullName() {
        return fullName;
    }

    public void setFullName(String fullName) {
        this.fullName = fullName;
    }

    public String getTitle() {
        return title;
    }

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

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }

    public int getStatus() {
        return status;
    }

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

    public Post getPost() {
        return post;
    }

    public void setPost(Post post) {
        this.post = post;
    }
}

 

// src/main/java/com/acesoftech/blog/entity/Post.java

package com.acesoftech.blog.entity;

import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.List; // ✅ Imported for List<Comment>

@Entity
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String slug;

    @Column(columnDefinition = "TEXT")
    private String content;

    private boolean featured;

    private boolean active;

    private String imageName;

    private LocalDateTime createdAt = LocalDateTime.now();

    @ManyToOne
    @JoinColumn(name = "category_id")
    private Category category;

    // ✅ ADDED: Relationship to Comment entity
    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Comment> comments;

    // Getters and Setters...

    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 getSlug() {
        return slug;
    }

    public void setSlug(String slug) {
        this.slug = slug;
    }

    public String getContent() {
        return content;
    }

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

    public boolean isFeatured() {
        return featured;
    }

    public void setFeatured(boolean featured) {
        this.featured = featured;
    }

    public boolean isActive() {
        return active;
    }

    public void setActive(boolean active) {
        this.active = active;
    }

    public String getImageName() {
        return imageName;
    }

    public void setImageName(String imageName) {
        this.imageName = imageName;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }

    public Category getCategory() {
        return category;
    }

    public void setCategory(Category category) {
        this.category = category;
    }

    // ✅ ADDED: Getter and Setter for comments
    public List<Comment> getComments() {
        return comments;
    }
 
    public void setComments(List<Comment> comments) {
        this.comments = comments;
    }
}

 

// src/main/java/com/acesoftech/blog/entity/User.java

package com.acesoftech.blog.entity;

import jakarta.persistence.*;

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;
    private String password;
    private String role;

    // Getters and Setters
}

 

REPOSITIORY FILES

// src/main/java/com/acesoftech/blog/repository/CategoryRepository.java

package com.acesoftech.blog.repository;

import com.acesoftech.blog.entity.Category;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface CategoryRepository extends JpaRepository<Category, Long> {
    Category findBySlug(String slug);
}

// src/main/java/com/acesoftech/blog/repository/CommentRepository.java

package com.acesoftech.blog.repository;

import com.acesoftech.blog.entity.Comment;
import com.acesoftech.blog.entity.Post;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface CommentRepository extends JpaRepository<Comment, Long> {
    List<Comment> findByPostId(Long postId);

    List<Comment> findByPostAndStatus(Post post, int i);
}

 

// src/main/java/com/acesoftech/blog/repository/PostRepository.java

package com.acesoftech.blog.repository;

import com.acesoftech.blog.entity.Category;
import com.acesoftech.blog.entity.Post;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
    Post findBySlug(String slug);

    List<Post> findByActiveTrueOrderByIdDesc();

    List<Post> findByCategoryAndActiveTrueOrderByIdDesc(Category category);

    List<Post> findTop3ByFeaturedTrueAndActiveTrueOrderByIdDesc();

    List<Post> findTop9ByActiveTrueOrderByIdDesc();
}

 

// src/main/java/com/acesoftech/blog/repository/UserRepository.java

package com.acesoftech.blog.repository;

import com.acesoftech.blog.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
}

 

📝 Template Files

 

<!– src/main/resources/templates/admin/layout.html –>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:fragment="layout">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title th:text="${pageTitle} ?: 'Admin Dashboard'">Admin</title>

    <!-- ✅ Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">

    <!-- ✅ Font Awesome -->
    <link rel="stylesheet"
          href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
          integrity="sha512-8IhzHNd4zBhZXhfDBb+fDX2MyjTxLCPtGPl6SVFe0upgq+dUYzdc8JSn1tUCGURQuHBXJ7JG4aH9D9VqEvK0Kg=="
          crossorigin="anonymous" referrerpolicy="no-referrer"/>

</head>
<body>

<!-- ✅ Admin Header -->
<div th:replace="admin/fragments/header :: header"></div>

<!-- ✅ Page Content -->
<main class="container mt-4" th:insert="~{::content}" style="min-height: 550px;"></main>

<!-- ✅ Admin Footer -->
<div th:replace="admin/fragments/footer :: footer"></div>

<!-- ✅ Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>

</body>
</html>

 

<!– src/main/resources/templates/admin/categories.html –>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="admin/layout :: layout">
<body>

<div th:fragment="content">
    <div class="container py-4">
        <h2 class="mb-4">Category List</h2>

        <div class="mb-3">
            <a class="btn btn-success" th:href="@{/admin/categories/new}">+ Add New Category</a>
        </div>

        <table class="table table-bordered table-striped">
            <thead class="table-dark">
                <tr>
                    <th>ID</th>
                    <th>Name</th>
                    <th>Slug</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="cat : ${categories}">
                    <td th:text="${cat.id}"></td>
                    <td th:text="${cat.name}"></td>
                    <td th:text="${cat.slug}"></td>
                    <td>
                        <a th:href="@{/admin/categories/edit/{id}(id=${cat.id})}" class="btn btn-sm btn-primary me-1">Edit</a>
                        <a th:href="@{/admin/categories/delete/{id}(id=${cat.id})}"
                           onclick="return confirm('Are you sure you want to delete this category?')"
                           class="btn btn-sm btn-danger">Delete</a>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</div>

</body>
</html>

 

<!– src/main/resources/templates/admin/category-form.html –>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="admin/layout :: layout">
<body>

<div th:fragment="content">
    <div class="container py-4">

        <h2 class="mb-4" th:text="${category.id != null} ? 'Edit Category' : 'Add New Category'"></h2>

        <form th:action="@{/admin/categories/save}" method="post" th:object="${category}" class="border p-4 rounded bg-light">
            <input type="hidden" th:field="*{id}"/>

            <!-- Parent Category Dropdown -->
            <div class="mb-3">
                <label for="parentId" class="form-label">Parent Category</label>
                <select id="parentId" class="form-select" th:field="*{parentId}">
                    <option value="">-- None --</option>
                    <option th:each="c : ${categories}"
                            th:value="${c.id}"
                            th:text="${c.name}"
                            th:selected="${c.id == category.parentId}">
                    </option>
                </select>
            </div>

            <div class="mb-3">
                <label for="name" class="form-label">Name</label>
                <input id="name" type="text" class="form-control" th:field="*{name}" required />
            </div>

            <div class="mb-3">
                <label for="slug" class="form-label">Slug</label>
                <input id="slug" type="text" class="form-control" th:field="*{slug}" readonly />
            </div>

            <div class="mb-3">
                <label for="description" class="form-label">Description</label>
                <textarea id="description" class="form-control" th:field="*{description}" rows="4"></textarea>
            </div>

            <div class="form-check mb-3">
                <input type="checkbox" class="form-check-input" id="active" th:field="*{active}" />
                <label class="form-check-label" for="active">Active</label>
            </div>

            <button type="submit" class="btn btn-primary">Save</button>
        </form>

    </div>

    <!-- ✅ Auto-Slug Generator Script -->
<script>
    document.addEventListener("DOMContentLoaded", function () {
        const nameInput = document.getElementById("name");
        const slugInput = document.getElementById("slug");

        nameInput.addEventListener("input", function () {
            const slug = nameInput.value
                .toLowerCase()
                .trim()
                .replace(/[^a-z0-9]+/g, "-")
                .replace(/^-+|-+$/g, "");
            slugInput.value = slug;
        });
    });
</script>

</div>


</body>
</html>

 

<!– src/main/resources/templates/admin/comments.html –>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="admin/layout :: layout">
<body>

<div th:fragment="content">
    <div class="container py-4">
        <h2 class="mb-4">Comment List</h2>

        <table class="table table-bordered table-striped">
            <thead class="table-dark">
                <tr>
                    <th>ID</th>
                    <th>Post Title</th>
                    <th>Full Name</th>
                    <th>Title</th>
                    <th>Description</th>
                    <th>Status</th>
                    <th>Created At</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="c : ${comments}">
                    <td th:text="${c.id}"></td>
                    <td th:text="${c.post.title}"></td>
                    <td th:text="${c.fullName}"></td>
                    <td th:text="${c.title}"></td>
                    <td th:text="${c.description}"></td>
                    <td>
                        <span th:text="${c.status == 1 ? 'Approved' : 'Pending'}"
                              th:classappend="${c.status == 1 ? 'text-success' : 'text-warning'}"></span>
                    </td>
                    <td th:text="${#temporals.format(c.createdAt, 'dd MMM yyyy')}"></td>
                    <td>
                        <a th:href="@{/admin/comments/approve/{id}(id=${c.id})}" 
                           class="btn btn-sm btn-success me-1">Approve</a>
                        <a th:href="@{/admin/comments/unapprove/{id}(id=${c.id})}" 
                           class="btn btn-sm btn-warning me-1">Unapprove</a>
                        <a th:href="@{/admin/comments/delete/{id}(id=${c.id})}"
                           onclick="return confirm('Are you sure you want to delete this comment?')"
                           class="btn btn-sm btn-danger">Delete</a>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</div>

</body>
</html>

 

<!– src/main/resources/templates/admin/dashboard.html –>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="admin/layout :: layout">
<body>

<div th:fragment="content">
    <h1>Welcome to the Admin Dashboard</h1>
    <p>This is your central control panel.</p>
</div>

</body>
</html>

 

<!– src/main/resources/templates/admin/login.html –>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="${pageTitle}">Admin Login</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-4">
            <div class="card shadow">
                <div class="card-body">
                    <h3 class="card-title text-center mb-4">Admin Login</h3>
                    <form method="post" action="/admin/login">
                        <div class="mb-3">
                            <label class="form-label">Username</label>
                            <input type="text" name="username" class="form-control" required>
                        </div>
                        <div class="mb-3">
                            <label class="form-label">Password</label>
                            <input type="password" name="password" class="form-control" required>
                        </div>
                        <button type="submit" class="btn btn-dark w-100">Login</button>

                        <div th:if="${param.error}" class="mt-3 alert alert-danger text-center">
                            Invalid credentials
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

 

<!– src/main/resources/templates/admin/post-form.html –>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="admin/layout :: layout">
<body>

<div th:fragment="content">
    <div class="container py-4">

        <h2 class="mb-4" th:text="${post.id != null} ? 'Edit Post' : 'Add New Post'"></h2>

        <form th:action="@{/admin/posts/save}" method="post" th:object="${post}" enctype="multipart/form-data" class="border p-4 rounded bg-light">
            <input type="hidden" th:field="*{id}" />

            <div class="mb-3">
                <label for="title" class="form-label">Title</label>
                <input id="title" type="text" class="form-control" th:field="*{title}" required />
            </div>

            <div class="mb-3">
                <label for="slug" class="form-label">Slug</label>
                <input id="slug" type="text" class="form-control" th:field="*{slug}" readonly />
            </div>

            <div class="mb-3">
                <label for="content" class="form-label">Content</label>
                <textarea id="content" class="form-control" rows="5" th:field="*{content}" required></textarea>
            </div>

            <div class="mb-3">
                <label for="category" class="form-label">Category</label>
                <select id="category" class="form-select" th:field="*{category.id}">
                    <option value="">-- Select Category --</option>
                    <option th:each="cat : ${categories}"
                            th:value="${cat.id}"
                            th:text="${cat.name}"
                            th:selected="${cat.id == post.category?.id}">
                    </option>
                </select>
            </div>

            <div class="mb-3">
                <div th:if="${post.imageName != null}">
                    <img th:src="@{'/uploads/' + ${post.imageName}}" alt="Uploaded Image"
                         class="img-thumbnail mb-2" width="150" height="100" style="object-fit: cover;" />
                </div>
                <label for="imageFile" class="form-label">Image</label>
                <input type="file" id="imageFile" name="imageFile" class="form-control" />
            </div>

            <div class="form-check form-switch mb-3">
                <input type="checkbox" class="form-check-input" id="featured" th:field="*{featured}" />
                <label class="form-check-label" for="featured">Featured</label>
            </div>

            <div class="form-check form-switch mb-3">
                <input type="checkbox" class="form-check-input" id="active" th:field="*{active}" />
                <label class="form-check-label" for="active">Active</label>
            </div>

            <button type="submit" class="btn btn-primary">Save Post</button>
        </form>
    </div>

    <script>
    document.addEventListener("DOMContentLoaded", function () {
        const titleInput = document.getElementById("title");
        const slugInput = document.getElementById("slug");

        titleInput.addEventListener("input", function () {
            const slug = titleInput.value.toLowerCase().trim()
                .replace(/[^a-z0-9]+/g, '-')
                .replace(/^-+|-+$/g, '');
            slugInput.value = slug;
        });
    });
</script>
</div>

<!-- ✅ Automatic Slug Generator -->


</body>
</html>

 

<!– src/main/resources/templates/admin/posts.html –>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="admin/layout :: layout">
<body>

<div th:fragment="content">
    <div class="container py-4">
        <h2 class="mb-4">Post List</h2>

        <div class="mb-3">
            <a th:href="@{/admin/posts/new}" class="btn btn-success">+ Add New Post</a>
        </div>

        <table class="table table-striped table-bordered">
            <thead class="table-dark">
                <tr>
                    <th>ID</th>
                    <th>Title</th>
                    <th>Slug</th>
                    <th>Image</th>
                    <th>Category</th>
                    <th>Featured</th>
                    <th>Active</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="post : ${posts}">
                    <td th:text="${post.id}"></td>
                    <td th:text="${post.title}"></td>
                    <td th:text="${post.slug}"></td>
                    <td>
                        <img th:src="@{'/uploads/' + ${post.imageName}}" alt="Post Image"
                             width="80" height="60" style="object-fit: cover;" />
                    </td>
                    <td th:text="${post.category.name}"></td>
                    <td>
                        <span class="badge bg-warning text-dark" th:if="${post.featured}">Featured</span>
                        <span class="badge bg-secondary" th:unless="${post.featured}">Normal</span>
                    </td>
                    <td>
                        <span class="badge bg-success" th:if="${post.active}">Active</span>
                        <span class="badge bg-danger" th:unless="${post.active}">Inactive</span>
                    </td>
                    <td>
                        <a th:href="@{/admin/posts/edit/{id}(id=${post.id})}" class="btn btn-sm btn-primary me-1">Edit</a>
                        <a th:href="@{/admin/posts/delete/{id}(id=${post.id})}"
                           onclick="return confirm('Are you sure you want to delete this post?')"
                           class="btn btn-sm btn-danger">Delete</a>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</div>

</body>
</html>

 

<!– src/main/resources/templates/admin/fragments/footer.html –>

<!-- templates/admin/fragments/footer.html -->
<div th:fragment="footer">
    <footer class="text-center mt-5 py-4 text-white" style="background: linear-gradient(135deg, #002272, #182848); box-shadow: 0 -2px 10px rgba(0,0,0,0.1);">
        <div class="container">
            <small>&copy; 2025 <strong>Admin Panel</strong>. All rights reserved.</small>
        </div>
    </footer>
</div>

 

<!– src/main/resources/templates/admin/fragments/header.html –>

<!-- templates/admin/fragments/header.html -->
<div th:fragment="header">
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container-fluid">
            <a class="navbar-brand d-flex align-items-center" th:href="@{/admin/dashboard}">
                <img src="https://www.acesoftech.com/wp-content/themes/acesoftech/assets/img/logo-mini/home-new-logo.png"
                     alt="Logo" style="height: 40px; margin-right: 10px;">
                <span>Admin Panel</span>
            </a>

            <!-- Toggle button for mobile view -->
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#adminNavbar"
                    aria-controls="adminNavbar" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>

            <!-- Navbar content -->
            <div class="collapse navbar-collapse" id="adminNavbar">
                <ul class="navbar-nav me-auto">
                    <li class="nav-item"><a class="nav-link" th:href="@{/admin/categories}">Categories</a></li>
                    <li class="nav-item"><a class="nav-link" th:href="@{/admin/posts}">Posts</a></li>
                    <li class="nav-item"><a class="nav-link" th:href="@{/admin/comments}">Comments</a></li>
                </ul>

                <!-- Logout Button -->
                <form th:action="@{/logout}" method="post" class="d-flex">
                    <button type="submit" class="btn btn-outline-light btn-sm">Logout</button>
                </form>
            </div>
        </div>
    </nav>
</div>

 

<!– src/main/resources/templates/front/about.html –>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="front/layout :: layout">
<body>
<div th:fragment="content">
    <div class="container py-4">
        <h1>About Us</h1>
        <p>Welcome to the blog. This page gives information about our mission and team.</p>
    </div>
</div>
</body>
</html>

 

<!– src/main/resources/templates/front/contact.html –>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="front/layout :: layout">
<body>
<div th:fragment="content">
    <div class="container py-5">
        <h1 class="mb-4">Contact Us</h1>

        <form method="post" action="/send-message">
            <div class="mb-3">
                <label for="name" class="form-label">Your Name</label>
                <input type="text" id="name" name="name" class="form-control" required placeholder="John Doe">
            </div>

            <div class="mb-3">
                <label for="email" class="form-label">Your Email</label>
                <input type="email" id="email" name="email" class="form-control" required placeholder="you@example.com">
            </div>

            <div class="mb-3">
                <label for="subject" class="form-label">Subject</label>
                <input type="text" id="subject" name="subject" class="form-control" placeholder="Subject">
            </div>

            <div class="mb-3">
                <label for="message" class="form-label">Message</label>
                <textarea id="message" name="message" rows="5" class="form-control" required placeholder="Write your message here..."></textarea>
            </div>

            <button type="submit" class="btn btn-primary">Send Message</button>
        </form>
    </div>
</div>
</body>
</html>

 

<!– src/main/resources/templates/front/index.html –>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="front/layout :: layout">
<body>

<div th:fragment="content">
    <div class="container mt-4">
        <div class="row">
            <!-- ✅ Main Blog Section -->
            <div class="col-md-9">
                <h2 class="mb-4">Latest Posts</h2>

                <div class="row" th:if="${posts.size() > 0}">
                    <div class="col-md-4 mb-4" th:each="post : ${posts}">
                        <div class="card h-100 shadow-sm">
                            <img th:if="${post.imageName != null}"
                                 th:src="@{'/uploads/' + ${post.imageName}}"
                                 class="card-img-top"
                                 alt="Post Image"
                                 style="height: 200px; object-fit: cover;">
                            <div class="card-body d-flex flex-column">
                                <h5 class="card-title" th:text="${post.title}">Post Title</h5>
                                <p class="card-text" th:text="${#strings.abbreviate(post.content, 100)}">Content...</p>
                                <a th:href="@{'/post/' + ${post.slug}}" class="btn btn-outline-primary mt-auto">Read More</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="alert alert-info" th:if="${posts.size() == 0}">
                    No blog posts available yet.
                </div>
            </div>

            <!-- ✅ Sidebar injected as fragment -->
            <div class="col-md-3">
                <div th:replace="front/fragments/sidebar :: sidebar"></div>
            </div>
        </div>
    </div>
</div>

</body>
</html>

 

<!– src/main/resources/templates/front/layout.html –>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:fragment="layout">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title th:text="${pageTitle} ?: 'Home - Blog'">Blog</title>

    <!-- ✅ Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">

    <!-- ✅ Font Awesome CDN -->
    <link rel="stylesheet"
          href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
          integrity="sha512-8IhzHNd4zBhZXhfDBb+fDX2MyjTxLCPtGPl6SVFe0upgq+dUYzdc8JSn1tUCGURQuHBXJ7JG4aH9D9VqEvK0Kg=="
          crossorigin="anonymous" referrerpolicy="no-referrer"/>

    <!-- ✅ Optional: Custom CSS -->
    <style>
        @media (max-width: 768px) {
            nav {
                flex-direction: column;
                align-items: flex-start;
                padding: 10px;
            }
            nav .navbar-brand {
                margin-bottom: 10px;
            }
            nav ul {
                flex-direction: column;
                width: 100%;
                padding-left: 0;
            }
            nav ul li {
                width: 100%;
                padding: 8px 0;
            }
            nav ul li a {
                display: block;
                width: 100%;
            }
        }
    </style>
</head>
<body>

    <!-- ✅ Header -->
    <div th:replace="front/fragments/header :: header"></div>

    <!-- ✅ Dynamic Page Content -->
    <div th:insert="~{::content}" style="min-height: 600px;"></div>

    <!-- ✅ Footer -->
    <div th:replace="front/fragments/footer :: footer"></div>

    <!-- ✅ Bootstrap JS -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>

</body>
</html>

 

<!– src/main/resources/templates/front/post-list-by-category.html –>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="${category.name} + ' - Category Posts'">Category</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
          integrity="sha512-8IhzHNd4zBhZXhfDBb+fDX2MyjTxLCPtGPl6SVFe0upgq+dUYzdc8JSn1tUCGURQuHBXJ7JG4aH9D9VqEvK0Kg=="
          crossorigin="anonymous" referrerpolicy="no-referrer"/>
</head>
<body>

<!-- ✅ Shared Header -->
<div th:replace="front/fragments/header :: header"></div>

<div class="container mt-4">
    <div class="row">
        <!-- ✅ Left Section: Posts in this Category -->
        <div class="col-md-9">
            <h2 th:text="'Posts in Category: ' + ${category.name}" class="mb-4"></h2>

            <div class="row" th:if="${posts.size() > 0}">
                <div class="col-md-4 mb-4" th:each="post : ${posts}">
                    <div class="card h-100 shadow-sm">
                        <img th:if="${post.imageName != null}" 
                             th:src="@{'/uploads/' + ${post.imageName}}" 
                             class="card-img-top" 
                             alt="Post Image" 
                             style="height: 200px; object-fit: cover;">
                        <div class="card-body d-flex flex-column">
                            <h5 th:text="${post.title}" class="card-title">Post Title</h5>
                            <p th:text="${#strings.abbreviate(post.content, 100)}" class="card-text"></p>
                            <a th:href="@{'/post/' + ${post.slug}}" class="btn btn-outline-primary mt-auto">Read More</a>
                        </div>
                    </div>
                </div>
            </div>

            <!-- Fallback: No posts in category -->
            <div class="alert alert-info" th:if="${posts.size() == 0}">
                No posts found in this category.
            </div>
        </div>

        <!-- ✅ Right Section: Sidebar -->
        <div class="col-md-3">
            <div th:replace="front/fragments/sidebar :: sidebar"></div>
        </div>
    </div>
</div>

<!-- ✅ Shared Footer -->
<div th:replace="front/fragments/footer :: footer"></div>

<!-- ✅ Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>

</body>
</html>

 

<!– src/main/resources/templates/front/post-view.html –>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="${post.title}">Post Title</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet"
          integrity="sha512-8IhzHNd4zBhZXhfDBb+fDX2MyjTxLCPtGPl6SVFe0upgq+dUYzdc8JSn1tUCGURQuHBXJ7JG4aH9D9VqEvK0Kg=="
          crossorigin="anonymous" referrerpolicy="no-referrer"/>
</head>
<body>

<!-- ✅ Shared Header -->
<div th:replace="front/fragments/header :: header"></div>

<div class="container mt-4">
    <div class="row">
        <!-- ✅ Main Post Content -->
        <div class="col-md-9">
            <h1 th:text="${post.title}">Post Title</h1>
            <p class="text-muted mb-3">
                <i class="fas fa-calendar-alt me-1"></i>
                <span th:text="'Published on: ' + ${#temporals.format(post.createdAt, 'dd MMM yyyy')}"></span>
            </p>

            <img th:if="${post.imageName != null}"
                 th:src="@{'/uploads/' + ${post.imageName}}"
                 class="img-fluid mb-4 rounded"
                 alt="Post Image" />

            <div th:if="${post.content != null}" th:utext="${post.content}" class="mb-5"></div>

            <!-- ✅ Comments Section -->
            <h3 class="mb-3"><i class="fas fa-comments me-2"></i>Comments</h3>

            <div th:if="${post.comments.size() > 0}">
                <div class="mb-4" th:each="comment : ${post.comments}">
                    <div class="border rounded p-3 mb-3 bg-light">
                        <h5 class="mb-1" th:text="${comment.title}">Comment Title</h5>
                        <small class="text-muted" th:text="'By ' + ${comment.fullName} + ' on ' + ${#temporals.format(comment.createdAt, 'dd MMM yyyy')}"></small>
                        <p class="mt-2 mb-0" th:text="${comment.description}"></p>
                    </div>
                </div>
            </div>
            <div th:if="${post.comments.size() == 0}" class="alert alert-info">
                <i class="fas fa-info-circle"></i> No comments yet. Be the first to comment!
            </div>

            <hr class="my-5">

            <!-- ✅ Comment Submission Form -->
            <h4 class="mb-3"><i class="fas fa-pen"></i> Leave a Comment</h4>
            <form th:action="@{'/post/' + ${post.slug} + '/comment'}" method="post">
                <input type="hidden" name="postId" th:value="${post.id}" />

                <div class="mb-3">
                    <label for="fullName" class="form-label">Full Name</label>
                    <input type="text" class="form-control" id="fullName" name="fullName" required>
                </div>

                <div class="mb-3">
                    <label for="title" class="form-label">Comment Title</label>
                    <input type="text" class="form-control" id="title" name="title" required>
                </div>

                <div class="mb-3">
                    <label for="description" class="form-label">Comment</label>
                    <textarea class="form-control" id="description" name="description" rows="4" required></textarea>
                </div>

                <button type="submit" class="btn btn-primary"><i class="fas fa-paper-plane"></i> Submit Comment</button>
            </form>

            <a href="/" class="btn btn-outline-secondary mt-4"><i class="fas fa-arrow-left"></i> Back to Home</a>
        </div>

        <!-- ✅ Sidebar -->
        <div class="col-md-3">
            <div th:replace="front/fragments/sidebar :: sidebar"></div>
        </div>
    </div>
</div>

<!-- ✅ Shared Footer -->
<div th:replace="front/fragments/footer :: footer"></div>

<!-- ✅ JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

<!– src/main/resources/templates/front/services.html –>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:replace="front/layout :: layout">
<body>
<div th:fragment="content">
    <div class="container py-5">
        <h1 class="mb-4">Our Services</h1>

        <div class="row">
            <div class="col-md-4 mb-4">
                <div class="card h-100 shadow-sm">
                    <div class="card-body">
                        <h5 class="card-title"><i class="fas fa-code me-2 text-primary"></i> Web Development</h5>
                        <p class="card-text">Custom web applications using modern technologies like Spring Boot, React, and more.</p>
                    </div>
                </div>
            </div>

            <div class="col-md-4 mb-4">
                <div class="card h-100 shadow-sm">
                    <div class="card-body">
                        <h5 class="card-title"><i class="fas fa-mobile-alt me-2 text-success"></i> Mobile Apps</h5>
                        <p class="card-text">Cross-platform Android/iOS mobile app development with performance and sleek UI.</p>
                    </div>
                </div>
            </div>

            <div class="col-md-4 mb-4">
                <div class="card h-100 shadow-sm">
                    <div class="card-body">
                        <h5 class="card-title"><i class="fas fa-search me-2 text-warning"></i> SEO & Marketing</h5>
                        <p class="card-text">Improve your website's visibility with our expert SEO and digital marketing strategies.</p>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

 

<!– src/main/resources/templates/front/fragments/footer.html –>

<!-- templates/front/fragments/footer.html -->
<div th:fragment="footer">
    <footer class="bg-dark text-white text-center py-3 mt-5">
        <div class="container">
            <p class="mb-0">&copy; <span th:text="${#dates.format(#dates.createNow(), 'yyyy')}">2025</span> Acesoftech Academy. All rights reserved.</p>
            <p class="mb-0 small">Developed by <strong>Acesoftech Academy Trainers</strong></p>
        </div>
    </footer>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</div>

 

<!– src/main/resources/templates/front/fragments/header.html –>

<!-- templates/front/fragments/header.html -->
<header th:fragment="header" style="background-color: black;">
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark container">
        <a class="navbar-brand d-flex align-items-center" th:href="@{/}">
            <img src="https://www.acesoftech.com/wp-content/themes/acesoftech/assets/img/logo-mini/home-new-logo.png"
                 alt="Logo"
                 style="height: 40px; margin-right: 10px;">
            <span>My Blog</span>
        </a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
                aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav ms-auto">
                <li class="nav-item"><a class="nav-link" th:href="@{/}">Home</a></li>
                <li class="nav-item"><a class="nav-link" th:href="@{/about}">About</a></li>
                <li class="nav-item"><a class="nav-link" th:href="@{/services}">Services</a></li>
                <li class="nav-item"><a class="nav-link" th:href="@{/contact}">Contact</a></li>
            </ul>
        </div>
    </nav>
</header>

 

<!– src/main/resources/templates/front/fragments/sidebar.html –>

<!-- templates/front/fragments/sidebar.html -->
<div th:fragment="sidebar">
    <div class="card shadow-sm mb-4">
        <div class="card-header bg-primary text-white">
            <i class="fas fa-folder-open"></i> Categories
        </div>
        <ul class="list-group list-group-flush">
            <li class="list-group-item" th:each="category : ${categories}">
                <i class="fas fa-folder text-secondary me-2"></i>
                <a th:href="@{'/category/' + ${category.id}}" th:text="${category.name}"></a>
            </li>
        </ul>
    </div>

    <!-- Optional: Future widget section -->
    <!--
    <div class="card shadow-sm mb-4">
        <div class="card-header bg-success text-white">
            🔥 Popular Posts
        </div>
        <ul class="list-group list-group-flush">
            <li class="list-group-item">Post A</li>
            <li class="list-group-item">Post B</li>
        </ul>
    </div>
    -->
</div>

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *