Skip to main content

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 file

    GET_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 called ProductList.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 called productList.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 file

    import ProductList from "../views/ProductList";

Step 5: Restart the extension server and relaunch the extension

QG1

  • Clicking on this arrow will redirect the user to the ProductList page

QG1

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.


Was this section helpful?