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
Endpoints
object in/app/src/service/endpoint.service.js
fileGET_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
MainService
object in/app/src/service/main-service.js
file// 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.jsx
and 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.css
and 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
CreateBrowserRouter
list in/app/src/router/index.js
file{
path: "/company/:company_id/:application_id/product-list/",
element: <ProductList />
}, -
Import
ProductList
component at the top of/app/src/router/index.js
fileimport 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.