Now, lets configure our Frontend as in previous workshop, we mostly focused on Backend. here, we will concentrate more on Frontend and how it is done.
Let us add functionalities - Clear explanation of code would be given in the video
let us add
Frontend
src/context/CartContext.jsx
import { createContext, useContext, useState } from "react";
const CartContext = createContext();
export function CartProvider({ children }) {
const [cartItems, setCartItems] = useState([]);
const addToCart = (product) => {
setCartItems((currentItems) => {
const existingItem = currentItems.find(
item => item.id === product.id
);
if (existingItem) {
return currentItems.map(item =>
item.id === product.id
? {
...item,
quantity: item.quantity + 1
}
: item
);
}
return [
...currentItems,
{
...product,
quantity: 1
}
];
});
};
const removeFromCart = (productId) => {
setCartItems(currentItems =>
currentItems.filter(item => item.id !== productId)
);
};
const increaseQuantity = (productId) => {
setCartItems(currentItems =>
currentItems.map(item =>
item.id === productId
? {
...item,
quantity: item.quantity + 1
}
: item
)
);
};
const decreaseQuantity = (productId) => {
setCartItems(currentItems =>
currentItems
.map(item =>
item.id === productId
? {
...item,
quantity: item.quantity - 1
}
: item
)
.filter(item => item.quantity > 0)
);
};
return (
{children}
);
}
export function useCart() {
return useContext(CartContext);
}
let us add components like Navigationbar and ProductCard
src/components/Navbar.jsx
command:
import { Link } from "react-router-dom";
import { useCart } from "../context/CartContext";
function Navbar() {
const { cartItems } = useCart();
const cartCount = cartItems.reduce(
(total, item) => total + item.quantity,
0
);
return (
<header className="navbar">
<div className="navbar-container">
<Link to="/" className="navbar-logo">
MAAYA STORE
</Link>
<nav className="navbar-links">
<Link to="/">
Products
</Link>
<Link to="/cart">
🛒 Cart ({cartCount})
</Link>
</nav>
</div>
</header>
);
}
export default Navbar;
src/components/ProductCard.jsx
command:
import { useNavigate } from "react-router-dom";
import { useCart } from "../context/CartContext";
function ProductCard({ product }) {
const navigate = useNavigate();
const { addToCart } = useCart();
const openProduct = () => {
navigate(`/products/${product.id}`);
};
const handleAddToCart = (event) => {
event.stopPropagation();
addToCart(product);
};
return (
<div
className="product-card"
onClick={openProduct}
>
<div className="product-image-container">
<img
src={`http://localhost:8080${product.imagePath}`}
alt={product.name}
className="product-image"
/>
</div>
<div className="product-info">
<h3>{product.name}</h3>
<p className="product-description">
{product.description}
</p>
<p className="product-price">
₹{product.price}
</p>
<p className="product-stock">
Stock: {product.stock}
</p>
<button
className="add-to-cart-button"
onClick={handleAddToCart}
>
Add to Cart
</button>
</div>
</div>
);
}
export default ProductCard;
Create pages for Product and Cart
command:
src/pages/ProductDetails.jsx
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useCart } from "../context/CartContext";
function ProductDetails() {
const { id } = useParams();
const navigate = useNavigate();
const { addToCart } = useCart();
const [product, setProduct] = useState(null);
const [error, setError] = useState("");
useEffect(() => {
fetch(`http://localhost:8080/api/products/${id}`)
.then(response => {
if (!response.ok) {
throw new Error("Product not found");
}
return response.json();
})
.then(data => {
setProduct(data);
})
.catch(error => {
console.error(error);
setError("Product not found");
});
}, [id]);
// Product not found
if (error) {
return (
<div className="product-details">
<div className="product-details-content">
<h2>{error}</h2>
<button
className="back-button"
onClick={() => navigate("/")}
>
← Back to Products
</button>
</div>
</div>
);
}
// Loading
if (!product) {
return (
<div className="product-details">
<div className="product-details-content">
<h2>Loading product...</h2>
</div>
</div>
);
}
// Product found
return (
<div className="product-details">
<div className="product-details-content">
<button
className="back-button"
onClick={() => navigate("/")}
>
← Back to Products
</button>
<div className="product-details-container">
{/* Product Image */}
<div className="product-details-image">
<img
src={`http://localhost:8080${product.imagePath}`}
alt={product.name}
/>
</div>
{/* Product Information */}
<div className="product-details-info">
<h1>{product.name}</h1>
<p className="product-details-description">
{product.description}
</p>
<p className="product-details-price">
₹{product.price}
</p>
<p>
Stock: {product.stock}
</p>
<button
className="add-to-cart-button"
onClick={() => {
addToCart(product);
}}
>
Add to Cart
</button>
</div>
</div>
</div>
</div>
);
}
export default ProductDetails;
Cart.jsx
command:
import { useCart } from "../context/CartContext";
function Cart() {
const {
cartItems,
removeFromCart,
increaseQuantity,
decreaseQuantity
} = useCart();
const total = cartItems.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return (
<div className="cart-page">
<h1>Your Cart</h1>
{cartItems.length === 0 ? (
<p>Your cart is empty.</p>
) : (
<>
{cartItems.map(item => (
<div
className="cart-item"
key={item.id}
>
<img
src={`http://localhost:8080${item.imagePath}`}
alt={item.name}
/>
<div>
<h3>{item.name}</h3>
<p>
₹{item.price}
</p>
<div>
<button
onClick={() =>
decreaseQuantity(item.id)
}
>
-
</button>
<span>
{" "}
{item.quantity}
{" "}
</span>
<button
onClick={() =>
increaseQuantity(item.id)
}
>
+
</button>
</div>
<button
onClick={() =>
removeFromCart(item.id)
}
>
Remove
</button>
</div>
</div>
))}
<h2>
Total: ₹{total}
</h2>
</>
)}
</div>
);
}
export default Cart;
update index.css
command:
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Arial, sans-serif;
background: #f5f5f5;
color: #222;
}
.app {
min-height: 100vh;
}
/* Header */
.header {
background: #04243D;
color: white;
padding: 20px 40px;
}
.header h1 {
margin: 0;
font-size: 28px;
}
/* Main */
.main-content {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
.main-content h2 {
text-align: center;
margin-bottom: 35px;
font-size: 32px;
}
/* Product Grid */
.product-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 25px;
}
/* Product Card */
.product-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
/* Product Image */
.product-image-container {
width: 100%;
height: 280px;
background: #f8f8f8;
display: flex;
align-items: center;
justify-content: center;
}
.product-image {
width: 100%;
height: 100%;
object-fit: contain;
}
/* Product Information */
.product-info {
padding: 20px;
}
.product-info h3 {
margin-top: 0;
font-size: 20px;
}
.product-description {
color: #666;
min-height: 40px;
}
.product-price {
font-size: 22px;
font-weight: bold;
margin: 15px 0 5px;
}
.product-stock {
color: #555;
}
/* Button */
.add-to-cart-button {
width: 100%;
padding: 12px;
border: none;
border-radius: 6px;
background: #5354FF;
color: white;
font-size: 16px;
cursor: pointer;
}
.add-to-cart-button:hover {
opacity: 0.9;
}
/* Responsive */
@media (max-width: 900px) {
.product-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 600px) {
.product-grid {
grid-template-columns: 1fr;
}
}
/* Product Details */
.product-details {
min-height: 100vh;
padding: 30px;
background: #f5f5f5;
}
.back-button {
border: none;
background: transparent;
font-size: 16px;
cursor: pointer;
margin-bottom: 25px;
}
.product-details-container {
max-width: 1100px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 40px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 50px;
}
.product-details-image {
height: 500px;
background: #f8f8f8;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
}
.product-details-image img {
width: 100%;
height: 100%;
object-fit: contain;
}
.product-details-info {
display: flex;
flex-direction: column;
justify-content: center;
}
.product-details-info h1 {
font-size: 36px;
margin-bottom: 15px;
}
.product-details-description {
color: #666;
font-size: 18px;
line-height: 1.6;
}
.product-details-price {
font-size: 30px;
font-weight: bold;
margin: 25px 0 10px;
}
.product-details-info .add-to-cart-button {
margin-top: 25px;
max-width: 300px;
}
@media (max-width: 700px) {
.product-details-container {
grid-template-columns: 1fr;
padding: 20px;
}
.product-details-image {
height: 350px;
}
}
.product-card {
cursor: pointer;
}
.product-card:hover {
transform: translateY(-4px);
transition: 0.2s;
}
/* =========================
NAVBAR
========================= */
.navbar {
width: 100%;
background-color: #04243D;
color: white;
}
.navbar-container {
max-width: 1200px;
margin: 0 auto;
padding: 18px 30px;
display: flex;
align-items: center;
justify-content: space-between;
}
.navbar-logo {
color: white;
text-decoration: none;
font-size: 24px;
font-weight: 700;
}
.navbar-links {
display: flex;
align-items: center;
gap: 30px;
}
.navbar-links a {
color: white;
text-decoration: none;
font-size: 16px;
}
.navbar-links a:hover {
opacity: 0.8;
}
update App.jsx
update main.jsx
Now when you try to click on any product, it does now show product details of that product even though we specifically created seperate page called ProductDetails.
This is because in the backend we did not allow specific Product Get method. Now let us configure it.
Add the following lines in ProductController.java
command:
@GetMapping("/{id}")
public Product getProductById(@PathVariable Long id) {
return productService.getProductById(id);
}
And now add the following lines in ProductService.Java
command:
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found"));
}
Now rebuild both frontend and backend with respective commands.
Now, if we check our productDetails page. we will have
you will have the product displayed.
Creating User Registration for Authentication.
Now let us create most Important for any website Users Data for Authentication.
create src/main/java/com/maayastore/backend/entity/User.java
create UserRepository.java
src/main/java/com/maayastore/backend/service/UserService.java
backend/src/main/java/com/maayastore/backend/controller/AuthController.java
Now test it using postman
Encrypting password
Now we will encrypt password, add dependency in pom.xml
//<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>//create backend/src/main/java/com/maaystore/backend/config/SecurityConfig.java
package com.maayastore.backend.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; @Configuration public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers( "/api/auth/**", "/api/products/**", "/images/**" ).permitAll() .anyRequest().authenticated() ); return http.build(); } }Update userservice.java
package com.maayastore.backend.service; import org.springframework.security.crypto.password.PasswordEncoder; // NEW LINE ADDED: Imports PasswordEncoder so we can encrypt the user's password. import org.springframework.stereotype.Service; import com.maayastore.backend.entity.User; import com.maayastore.backend.repository.UserRepository; @Service public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; // NEW LINE ADDED: Creates a reference to PasswordEncoder. // We will use this to encrypt the user's password before saving it. public UserService( UserRepository userRepository, PasswordEncoder passwordEncoder) { // CHANGED: Constructor now receives PasswordEncoder in addition to UserRepository. // Spring will automatically provide both objects to this constructor. this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; // NEW LINE ADDED: Stores the PasswordEncoder that Spring provided. } public User registerUser(User user) { String encodedPassword = passwordEncoder.encode(user.getPassword()); // NEW LINES ADDED: // Takes the user's plain-text password // and converts it into a BCrypt hashed password. // // Example: // "123456" // ↓ // passwordEncoder.encode(...) // ↓ // "$2a$10$...." user.setPassword(encodedPassword); // NEW LINE ADDED: // Replaces the plain-text password inside the User object // with the encoded/hashed password. return userRepository.save(user); } }
Now rebuild our application
command:
mvn spring-boot:run
Now if you check output in postman.
Now if you check our database

This means our user password is now in hash format, but we dont want our password to leave backend as you
can see from POSTMAN, our hashpassword is given as response for user input.
To avoid that, we will use DTO.
Data Transfer Object
backend/src/main/java/com/maaystore/backend/dto/UserResponse.java
package com.maayastore.backend.dto; public class UserResponse { private Long id; private String name; private String email; private String role; public UserResponse() { } public UserResponse(Long id, String name, String email, String role) { this.id = id; this.name = name; this.email = email; this.role = role; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } }
Notice that there is no password in this file.
What are we doing here?, whenever user is registered. we are focusing to save it in database and not to expose
password as response back. response can be username, email or password.
But we will be avoiding password as return response.
Now let us update UserService.Java to include our DTO
command:
package com.maayastore.backend.service; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import com.maayastore.backend.dto.UserResponse; import com.maayastore.backend.entity.User; import com.maayastore.backend.repository.UserRepository; @Service public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService( UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public UserResponse registerUser(User user) { // CHANGED: Previously this returned User. // Now it returns UserResponse so that sensitive information, // such as the password hash, is not sent back to the frontend. String encodedPassword = passwordEncoder.encode(user.getPassword()); // NEW: Hashes the user's password using PasswordEncoder. // // Example: // User enters: "123456" // // Before: // user.getPassword() → "123456" // // After: // encodedPassword → "$2a$10$........" // // The actual password is NOT stored directly in the database. user.setPassword(encodedPassword); User savedUser = userRepository.save(user); // CHANGED: Previously we directly returned: // return userRepository.save(user); // // Now we save the user and store the result in savedUser. // We need savedUser to get the user's details and create // a UserResponse object. return new UserResponse( savedUser.getId(), savedUser.getName(), savedUser.getEmail(), savedUser.getRole() ); // NEW: Creates and returns a UserResponse DTO. // // Only safe information is returned: // - id // - name // - email // - role // // Notice that password is NOT included. // Therefore, the password hash is not exposed to the frontend. } }
Now update AuthController.Java
Handling Duplication
Update UserService.java with following
src/main/java/com/maaystore/backend/dto/ErrorResponse.java
command:
package com.maayastore.backend.dto; public class ErrorResponse { private String message; public ErrorResponse(String message) { this.message = message; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
but now we need to check whenever user enters the same mail id, it has to check in the existing databaseand if the mail id is already present throw an errorand if the mail id is not present, continue on creating new user.so we will update our POST API for register in AuthController.java
Adding Validation:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
also update Entity/User.java
Now update even AuthController.java
Why are we using GlobalException, because we can use same set of rules when we design login page
as well , like user cannot give empty mail id or password to login.
backend/src/main/java/com/maayastore/backend/exception/GlobalExceptionHandler.java
command:
package com.maayastore.backend.exception; import java.util.HashMap; import java.util.Map; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Map<String, String>> handleValidationErrors( MethodArgumentNotValidException exception) { Map<String, String> errors = new HashMap<>(); exception.getBindingResult() .getFieldErrors() .forEach(error -> errors.put( error.getField(), error.getDefaultMessage() ) ); return ResponseEntity.badRequest().body(errors); } }
Rebuild application and verify postman logs, you will be able to see the messages now
Setting User´s Default role.
a simple change can avoid this. add user.setRole("USER"); in AuthController.java
package com.maayastore.backend.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.maayastore.backend.dto.ErrorResponse; import com.maayastore.backend.entity.User; import com.maayastore.backend.service.UserService; import jakarta.validation.Valid; @RestController @RequestMapping("/api/auth") public class AuthController { private final UserService userService; public AuthController(UserService userService) { this.userService = userService; } @PostMapping("/register") public ResponseEntity<?> registerUser( @Valid @RequestBody User user) { if (userService.emailExists(user.getEmail())) { return ResponseEntity .badRequest() .body(new ErrorResponse("Email already registered")); } user.setRole("USER"); // Security: Always assign the default USER role on registration. // This prevents clients from submitting "ADMIN" as their role. return ResponseEntity.ok( userService.registerUser(user) ); } }
Now rebuild our application and test using POSTMAN
Building Registration Page.
frontend/src/pages/Register.jsx
Now update App.jsx
Also update index.css for styling our registration page
.register-page {
min-height: calc(100vh - 70px);
display: flex;
justify-content: center;
align-items: center;
background: #f7f8fa;
padding: 40px 20px;
}
.register-container {
width: 100%;
max-width: 430px;
background: white;
padding: 40px;
border-radius: 14px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
}
.register-container h2 {
margin: 0 0 30px;
text-align: center;
font-size: 28px;
color: #04243D;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
color: #333;
}
.form-group input {
width: 100%;
box-sizing: border-box;
padding: 13px 14px;
border: 1px solid #d9dde3;
border-radius: 8px;
font-size: 15px;
outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.form-group input:focus {
border-color: #5354FF;
box-shadow: 0 0 0 3px rgba(83, 84, 255, 0.12);
}
.form-group input::placeholder {
color: #999;
}
.register-container button {
width: 100%;
padding: 14px;
margin-top: 8px;
border: none;
border-radius: 8px;
background: #04243D;
color: white;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease, transform 0.1s ease;
}
.register-container button:hover {
background: #053452;
}
.register-container button:active {
transform: scale(0.99);
}
.register-container button:disabled {
background: #9da5ad;
cursor: not-allowed;
transform: none;
}
.success-message {
padding: 12px 14px;
margin-bottom: 20px;
border-radius: 8px;
background: #edf8f0;
color: #2e7d32;
font-size: 14px;
}
.error-message {
padding: 12px 14px;
margin-bottom: 20px;
border-radius: 8px;
background: #fff0f0;
color: #c62828;
font-size: 14px;
}
.field-error {
margin: 6px 0 0;
color: #d32f2f;
font-size: 13px;
}
@media (max-width: 500px) {
.register-page {
padding: 25px 15px;
}
.register-container {
padding: 30px 22px;
}
.register-container h2 {
font-size: 24px;
}
}
Now let us rebuild our frontend application
command:
npm run dev



















Comments
Post a Comment