Product list page
Step 1: Create Backend APIs
-
Add the following highlighted code to the
/com/fynd/example/java/controller/ProductController.java.package com.fynd.example.java.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fynd.example.java.db.ProductHighlight;
import com.fynd.example.java.db.interfaces.ProductHighlightRepository;
import com.fynd.extension.controllers.BasePlatformController;
import com.fynd.extension.session.Session;
import com.sdk.platform.PlatformClient;
import com.sdk.platform.configuration.ConfigurationPlatformModels.ApplicationsResponse;
import com.sdk.platform.configuration.ConfigurationPlatformModels.Application;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.HttpStatus;
import java.util.*;
@RestController
@RequestMapping("/api/v1.0")
@Slf4j
public class ProductController extends BasePlatformController {
@Autowired
ProductHighlightRepository productHighlightRepository;
@Autowired
private ObjectMapper mapper;
@GetMapping(value = "/applications")
public ResponseEntity<ApplicationsResponse> getApplications(HttpServletRequest request) {
try {
PlatformClient platformClient = (PlatformClient) request.getAttribute("platformClient");
Session fdkSession = (Session) request.getAttribute("fdkSession");
String companyId = fdkSession.getCompanyId();
ApplicationsResponse applications
= platformClient.configuration.getApplications(1, 100, mapper.writeValueAsString(Collections.singletonMap("is_active", true)));
Set<String> activeApplicationSet = new HashSet<>();
List<ProductHighlight> productSchema = productHighlightRepository.findByCompanyIdAndIsActive(companyId);
for (ProductHighlight product: productSchema) {
activeApplicationSet.add(product.getApplicationId().toString());
}
for (Application application: applications.getItems()) {
application.setIsActive(activeApplicationSet.contains(application.getId()));
}
return ResponseEntity.ok(applications);
} catch(Exception e) {
System.out.println(e.getMessage());
throw new RuntimeException(e);
}
}
@GetMapping(value = "/{application_id}/highlight/list")
public ResponseEntity<List<ProductHighlight>> getProductHighlightList(
@PathVariable("application_id") String applicationId,
HttpServletRequest request
) {
try {
Session fdkSession = (Session) request.getAttribute("fdkSession");
String companyId = fdkSession.getCompanyId();
List<ProductHighlight> data
= productHighlightRepository.findByCompanyIdAndApplicationId(companyId, applicationId);
return ResponseEntity.ok(data);
} catch (Exception e) {
System.out.println(e.getMessage());
throw new RuntimeException(e);
}
}
@GetMapping(value = "/{application_id}/highlight")
public ResponseEntity getProductHighlight(
@PathVariable("application_id") String applicationId,
@RequestParam(value = "slug", required = false) String slug,
@RequestParam(value = "item_code", required = false) Integer itemCode,
HttpServletRequest request
) {
try {
Session fdkSession = (Session) request.getAttribute("fdkSession");
String companyId = fdkSession.getCompanyId();
Optional<ProductHighlight> data;
if (itemCode != null) {
data = productHighlightRepository.findOneByCompanyIdAndApplicationIdAndProductItemCode(companyId, applicationId, itemCode);
} else if (slug != null) {
data = productHighlightRepository.findOneByCompanyIdAndApplicationIdAndProductSlug(companyId, applicationId, slug);
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid item_code or slug in query param");
}
return ResponseEntity.status(HttpStatus.OK).body(data);
} catch (Exception e) {
System.out.println(e.getMessage());
throw new RuntimeException(e);
}
}
@DeleteMapping(value = "/{application_id}/highlight")
public ResponseEntity deleteProductHighlight(
@PathVariable("application_id") String applicationId,
@RequestParam(value = "slug", required = false) String slug,
@RequestParam(value = "item_code", required = false) Integer itemCode,
HttpServletRequest request
) {
try {
Session fdkSession = (Session) request.getAttribute("fdkSession");
String companyId = fdkSession.getCompanyId();
if (itemCode != null) {
productHighlightRepository.deleteOneByCompanyIdAndApplicationIdAndProductItemCode(companyId, applicationId, itemCode);
} else if (itemCode != null) {
productHighlightRepository.deleteOneByCompanyIdAndApplicationIdAndProductSlug(companyId, applicationId, slug);
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid item_code or slug in query param");
}
return ResponseEntity.status(HttpStatus.NO_CONTENT).body("Product highlight deleted");
} catch (Exception e) {
System.out.println(e.getMessage());
throw new RuntimeException(e);
}
}
} -
Also, add the following endpoints to the
Endpointsobject in/app/src/service/endpoint.service.jsfileGET_HIGHLIGHT_LIST(application_id) {
return urlJoin(envVars.EXAMPLE_MAIN_URL, `api/v1.0/${application_id}/highlight/list`);
},
PRODUCT_HIGHLIGHT(application_id) {
return urlJoin(envVars.EXAMPLE_MAIN_URL, `api/v1.0/${application_id}/highlight`);
}, -
Also, add the following methods to the
MainServiceobject in/app/src/service/main-service.jsfile// product highlights
getHighlightList(application_id) {
return axios.get(URLS.GET_HIGHLIGHT_LIST(application_id));
},
getProductHighlight(application_id, item_code="", slug="") {
return axios.get(URLS.PRODUCT_HIGHLIGHT(application_id), {params: {item_code, slug}});
},
deleteProductHighlight(application_id, item_code="", slug="") {
return axios.delete(URLS.PRODUCT_HIGHLIGHT(application_id), {params: {item_code, slug}});
},
Step 2: Create FrontEnd Page
-
In the
/app/src/views/directory create a new file calledProductList.jsxand add the following code to the file:import React, { useEffect, useState, useRef } from "react";
import Loader from "../components/Loader";
import { Button, Input, ToggleButton, SvgIcEdit, SvgIcTrash, SvgIcArrowBack } from "@gofynd/nitrozen-react";
import { useParams, useNavigate } from 'react-router-dom'
import styles from './style/productList.module.css';
import MainService from "../services/main-service";
function ProductCard({productItem, onProductDelete}) {
const [toggleState, setToggleState] = useState(productItem.is_active);
const { company_id, application_id } = useParams();
const dummyState = useRef(false);
const navigate = useNavigate();
useEffect(() => {
if (dummyState.current) {
(async () => {
if (toggleState) {
await MainService.addInjectableTag(application_id, productItem.product_item_code);
} else {
await MainService.deleteInjectableTag(application_id, productItem.product_item_code);
}
})()
}
dummyState.current = true;
}, [application_id, productItem.product_item_code, toggleState])
return (
<>
<div className={styles.product_card}>
<div className={styles.card_left}>
{/* PRODUCT LOGO */}
<div className={styles.image_card}>
<img className={styles.logo} src={productItem.product.image} alt="product_image" />
</div>
{/* PRODUCT META */}
<div className={styles.product_metadata}>
<div className={styles.product_metadata_header}>
<div className={styles.header_name}>
{productItem.product.name}
</div>
<div className={styles.pipe}>
|
</div>
<span className={styles.item_code}>
Item code: {productItem.product_item_code}
</span>
</div>
<div className={styles.product_metadata_brand}>
{productItem.product.brand_name}
</div>
<div className={styles.product_metadata_category}>
category: {productItem.product.category_slug}
</div>
</div>
</div>
{/* TOGGLE BUTTON */}
<div className={styles.product_toggle_button}>
<ToggleButton
id={productItem.product_item_code}
size={"small"}
value={toggleState}
onToggle={async (event) => {
setToggleState((pre) => !pre);
}}
/>
</div>
<div className={styles.product_delete_edit}>
{/* DELETE SVG */}
<div>
<SvgIcTrash
color="#2E31BE"
className={styles.product_delete}
onClick={async () => {
if (toggleState) {
await MainService.deleteInjectableTag(application_id, productItem.product_item_code);
}
await MainService.deleteProductHighlight(application_id, productItem.product_item_code);
onProductDelete(productItem.product_item_code);
}}
/>
</div>
{/* EDIT SVG */}
<div>
<SvgIcEdit
color="#2E31BE"
className={styles.product_edit}
onClick={() => {
navigate(`/company/${company_id}/${application_id}/highlight/${productItem.product_item_code}`);
}}
/>
</div>
</div>
</div>
</>
)
}
export default function ProductList() {
const [pageLoading, setPageLoading] = useState(false);
const [productItems, setProductItems] = useState([]);
const [allProductItems, setAllProductItems] = useState([]);
const [searchTextValue, setSearchTextValue] = useState("");
const navigate = useNavigate();
const { company_id, application_id } = useParams();
async function fetchProductItems() {
const { data } = await MainService.getHighlightList(application_id);
setAllProductItems(data);
setProductItems(data);
setPageLoading(false);
}
function createProductHighlights() {
navigate(`/company/${company_id}/${application_id}/highlight/create`)
}
function onProductDelete(product_item_code) {
setAllProductItems((prevState) => {
let findIndex = prevState.findIndex(product => product.product_item_code === product_item_code);
prevState.splice(findIndex, 1);
let newArr = [...prevState]
return newArr;
})
}
useEffect(() => {
if (!searchTextValue) {
setProductItems(allProductItems.map((product) => product))
} else {
setProductItems(
allProductItems.filter((item) => {
return item.product.name.toLowerCase().includes(searchTextValue.toLowerCase());
})
)
}
}, [allProductItems, searchTextValue]);
useEffect(() => {
setPageLoading(true);
fetchProductItems()
}, []);
return (
<>
{ pageLoading ? (
<Loader />
) : (
<div className={styles.main_wrapper}>
<div className={styles.sticky_header}>
<div className={styles.navbar_left_section}>
{/* BACK ARROW */}
<div className={styles.back_arrow}>
<SvgIcArrowBack
color='#2E31BE'
style={{
width: "24px",
height: "auto"
}}
onClick={() => {
navigate(`/company/${company_id}/`);
}}
/>
</div>
{/* SEARCH INPUT */}
<div className={styles.search_product_highlight}>
<Input
showSearchIcon
className={styles.search_input}
type="text"
placeholder="search by product name"
value={searchTextValue}
disabled={Object.keys(allProductItems).length === 0 ? true : false }
onChange={(event) => {
setSearchTextValue(event.target.value);
}}
/>
</div>
</div>
{/* CREATE HIGHLIGHT BUTTON */}
<div className={styles.create_highlight_button}>
<Button
onClick={() => {createProductHighlights()}}
rounded={false}
>
Create Product Highlight
</Button>
</div>
</div>
<div className={styles.product_listing}>
{productItems.map((product) => (
<ProductCard
key={product.product_item_code}
productItem={product}
onProductDelete={onProductDelete}
/>
))}
</div>
</div>
)}
</>
);
}
Step 3: Add CSS for the ProductList component
-
In the
/app/src/views/style/directory create a new file calledproductList.module.cssand add the following code to the file.main_wrapper {
font-family: Inter;
position: relative;
box-sizing: border-box;
background: #fff;
border: 1px solid #f3f3f3;
border-radius: 12px;
padding: 24px;
margin: 24px;
}
.sticky_header {
background-color: #ffffff;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.back_arrow {
cursor: pointer;
}
.navbar_left_section {
display: flex;
align-items: center;
}
/* button */
.create_highlight_button {
margin: 12px 12px;
max-height: 48px;
}
/* Product search */
.search_product_highlight {
max-width: 480px;
min-width: 480px;
margin: 0 24px;
}
.search_input {
height: 18px;
}
/* Product listing */
.product_listing {
margin: 0 16px;
padding-top: 24px;
}
.product_card {
display: flex;
justify-content: space-between;
border: 1px solid #e4e5e6;
min-height: 102px;
padding: 16px;
border-radius: 4px;
margin-bottom: 16px;
box-sizing: border-box;
transition: box-shadow 0.3s;
}
.product_card:hover {
box-shadow: 0px 9px 13px 0px rgba(221, 221, 221, 0.5);
}
.product_card .card_left {
display: flex;
flex-direction: row;
align-items: center;
flex: 2;
}
.product_card .image_card {
min-height: 60px;
min-width: 60px;
max-height: 60px;
max-width: 60px;
display: flex;
align-items: center;
margin: 0 12px;
}
.product_card .image_card .logo {
height: 60px;
width: 100%;
object-fit: cover;
border-radius: 50%;
}
.product_metadata {
display: flex;
flex-direction: column;
}
.product_metadata_header {
display: flex;
flex-direction: row;
align-items: center;
}
.product_metadata_header .header_name {
line-height: 21px;
margin-right: 10px;
color: #41434C;
font-weight: 600;
font-size: 14px;
-webkit-font-smoothing: antialiased;
}
.product_metadata_header .pipe {
line-height: 20px;
margin-right: 10px;
color: #9B9B9B;
font-weight: 400;
font-size: 12px;
-webkit-font-smoothing: antialiased;
}
.product_metadata_header .item_code {
line-height: 20px;
color: #9B9B9B;
font-weight: 400;
font-size: 12px;
-webkit-font-smoothing: antialiased;
}
.product_metadata_brand, .product_metadata_category {
color: #666666;
line-height: 21px;
font-weight: 400;
font-size: 12px;
-webkit-font-smoothing: antialiased;
}
.product_toggle_button {
flex: 0.5;
display: flex;
align-items: center;
justify-content: center;
}
.product_delete_edit {
display: flex;
flex-direction: row;
flex: 0.5;
align-items: center;
justify-content: space-evenly;
}
.product_delete, .product_edit {
height: 24px;
width: auto;
margin: 12px;
}
.product_edit:hover, .product_delete:hover {
cursor: pointer;
}
Step 4: Create a Route for ProductList component
-
Using react-router to create routes for the Product List page
-
Add the following objects to the
CreateBrowserRouterlist in/app/src/router/index.jsfile{
path: "/company/:company_id/:application_id/product-list/",
element: <ProductList />
}, -
Import
ProductListcomponent at the top of/app/src/router/index.jsfileimport ProductList from "../views/ProductList";
Step 5: Restart the extension server and relaunch the extension
![]()
- Clicking on this arrow will redirect the user to the ProductList page
![]()
Currently we don’t have highlights created for the products, that’s why any products are not showing.
In order to create product highlights we’ll need to create CreateProductHighlight page.