Create product highlight page
Step 1: Create Backend APIs
Below helper models will be necessary for creating the API.
-
Create
/com/fynd/example/java/helper/models/ProductHighlightRequest.javafile and add following code:package com.fynd.example.java.helper.models;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class ProductHighlightRequest {
@JsonProperty("product_meta")
private ProductMeta productMeta;
@JsonProperty("highlights")
private List<String> highlights;
@JsonProperty("enablePriceDrop")
private Boolean enablePriceDrop;
} -
Create
/com/fynd/example/java/helper/models/ProductMeta.javafile and add following code:package com.fynd.example.java.helper.models;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ProductMeta {
@JsonProperty("name")
private String name;
@JsonProperty("product_item_code")
private int productItemCode;
@JsonProperty("product_slug")
private String productSlug;
@JsonProperty("image")
private String image;
@JsonProperty("brand_name")
private String brandName;
@JsonProperty("category_slug")
private String categorySlug;
@JsonProperty("price")
private Price price;
} -
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 com.fynd.example.java.db.Product;
import com.sdk.platform.catalog.CatalogPlatformModels.*;
import com.fynd.example.java.helper.models.ProductHighlightRequest;
import com.fynd.example.java.helper.models.ProductMeta;
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}/products")
public ResponseEntity<RawProductListingResponse> getProducts(
@PathVariable("application_id") String applicationId,
@RequestParam(value = "query", required = false) String query,
HttpServletRequest request
) {
try {
PlatformClient platformClient = (PlatformClient) request.getAttribute("platformClient");
RawProductListingResponse response = platformClient.application(applicationId).catalog.getAppProducts(
null, null, null, null,null, 1, 10, query
);
return ResponseEntity.ok(response);
} catch (Exception e) {
System.out.println(e.getMessage());
throw new RuntimeException(e);
}
}
@PostMapping(value = "/{application_id}/product/{item_id}/highlights")
public ResponseEntity<ProductHighlight> updateProductHighlight(
@PathVariable("application_id") String applicationId,
@PathVariable("item_id") Integer itemId,
@RequestBody ProductHighlightRequest requestBody,
HttpServletRequest request
) {
try {
Session fdkSession = (Session) request.getAttribute("fdkSession");
String companyId = fdkSession.getCompanyId();
List<String> highlights = requestBody.getHighlights();
Boolean enablePriceDrop = requestBody.getEnablePriceDrop();
ProductMeta productMeta = requestBody.getProductMeta();
Optional<ProductHighlight> data
= productHighlightRepository.findOneByCompanyIdAndApplicationIdAndProductItemCode(companyId, applicationId, itemId);
ProductHighlight productHighlight;
if (data.isPresent()) {
productHighlight = data.get();
productHighlight.getProduct().setHighlights(highlights);
productHighlight.getProduct().setEnablePriceDrop(enablePriceDrop);
} else {
productHighlight = new ProductHighlight(
companyId,
applicationId,
productMeta.getProductItemCode(),
productMeta.getProductSlug(),
new Product(
productMeta.getName(),
productMeta.getImage(),
productMeta.getBrandName(),
productMeta.getCategorySlug(),
highlights,
productMeta.getPrice(),
enablePriceDrop
),
false
);
}
productHighlightRepository.save(productHighlight);
return ResponseEntity.ok(productHighlight);
} 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_PRODUCTS(application_id) {
return urlJoin(envVars.EXAMPLE_MAIN_URL, `api/v1.0/${application_id}/products`);
},
CREATE_PRODUCT_HIGHLIGHTS(application_id, item_id) {
return urlJoin(envVars.EXAMPLE_MAIN_URL, `api/v1.0/${application_id}/product/${item_id}/highlights`);
}, -
Also, add the following methods to the
MainServiceobject in/app/src/service/main-service.jsfilegetAllProducts(application_id, query) {
return axios.get(URLS.GET_PRODUCTS(application_id), {params: {query}});
},
createProductHighlights(application_id, item_id, data) {
return axios.post(URLS.CREATE_PRODUCT_HIGHLIGHTS(application_id, item_id), data);
},
Step 2: Create FrontEnd page
-
In the
/app/src/views/directory create a new file calledCreateHighlight.jsx -
Add the following code to the file
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Button, Dropdown, Input, Checkbox, SvgIcConfirm, SvgIcArrowBack, SvgIcTrash
} from '@gofynd/nitrozen-react';
import MainService from '../services/main-service';
import styles from './style/createHighlight.module.css'
export default function CreateHighlight() {
// page params
const { company_id, application_id, item_code } = useParams();
// navigation instance
const navigate = useNavigate();
// application product list
const [productItems, setProductItems] = useState([]);
const [searchText, setSearchText] = useState('');
// highlight text input
const [highlightInput, setHighlightInput] = useState("");
// locally maintained highlight list
const [highlightList, setHighlightList] = useState([]);
// current selected dropdown value
const [selectedDropdownProduct, setSelectedDropdownProduct] = useState({});
// is edit page
const [isEdit, setIsEdit] = useState(false);
const [editProduct, setEditProduct] = useState({});
// price drop
const [checkboxValue, setCheckboxValue] = useState(false);
// handle dropdown search
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
console.log(searchText)
getApplicationProductList();
}, 500)
return () => clearTimeout(delayDebounceFn)
}, [searchText])
// application product list for dropdown
const getApplicationProductList = async () => {
if (item_code) {
setIsEdit(true);
const { data } = await MainService.getProductHighlight(application_id, item_code);
setHighlightList(data?.product?.highlights);
setCheckboxValue(data?.product?.enablePriceDrop);
setEditProduct({
name: data?.product?.name,
product_slug: data?.product_slug,
image: data?.product?.image,
brand_name: data?.product?.brand_name,
category_slug: data?.product?.category_slug,
product_item_code: data?.product_item_code
})
} else {
const { data } = await MainService.getAllProducts(application_id, searchText);
setProductItems(data.items);
}
}
// handle dropdown onChange
const dropdownChangeHandler = async (productMeta) => {
let { data } = await MainService.getProductHighlight(application_id, productMeta.product_item_code);
if (data) {
setHighlightList(data.product.highlights);
setCheckboxValue(data.product.enablePriceDrop);
} else {
setHighlightList([]);
setCheckboxValue(false);
}
setSelectedDropdownProduct(productMeta);
}
// Dropdown data
const getSearchItems = () => {
let prepareProductList = []
productItems.forEach((product) => {
let searchProduct = {}
searchProduct.text = product?.name
searchProduct.sub_text = product?.brand?.name
searchProduct.value = {
name: product?.name,
product_slug: product?.slug,
image: product?.images[0],
brand_name: product?.brand?.name,
category_slug: product?.category_slug,
product_item_code: product?.uid,
price: product?.price
}
searchProduct.logo = product?.images[0]
prepareProductList.push(searchProduct);
})
return prepareProductList;
}
const handleSubmit = async () => {
if (isEdit) {
await MainService.createProductHighlights(
application_id,
editProduct.product_item_code,
{
productMeta: editProduct,
highlights: highlightList,
enablePriceDrop: checkboxValue
}
)
} else {
await MainService.createProductHighlights(
application_id,
selectedDropdownProduct.product_item_code,
{
product_meta: selectedDropdownProduct,
highlights: highlightList,
enablePriceDrop: checkboxValue
}
)
}
navigate(`/company/${company_id}/${application_id}/product-list/`);
}
return (
<>
<div className={styles.main_wrapper}>
{/* NAVBAR */}
<div className={styles.navbar}>
{/* NAVBAR LEFT */}
<div className={styles.navbar_left_header}>
<div className={styles.back_arrow}>
<SvgIcArrowBack
color='#2E31BE'
style={{
width: "24px",
height: "auto"
}}
onClick={() => {
navigate(`/company/${company_id}/${application_id}/product-list/`)
}}
/>
</div>
<div className={styles.main_title}>
{isEdit ? ("Edit") : ("Create")} Product Highlight
</div>
</div>
{/* NAVBAR RIGHT */}
<div className={styles.navbar_buttons}>
{/* DISCARD BUTTON */}
<div>
<Button
state='default'
theme='secondary'
// size='small'
rounded={false}
onClick={() => {
navigate(`/company/${company_id}/${application_id}/product-list/`)
}}
>
Discard
</Button>
</div>
{/* SUBMIT BUTTON */}
<div>
<Button
state='default'
theme='primary'
// size='small'
rounded={false}
onClick={handleSubmit}
>
{isEdit ? ("Save") : ("Submit")}
</Button>
</div>
</div>
</div>
{/* END NAVBAR */}
<div className={styles.content_wrapper}>
<div className={styles.highlight_detail_box}>
<div>
<div className={styles.highlight_detail_box_header}>Product Highlight Detail</div>
</div>
{/* PRODUCT DROPDOWN */}
{!isEdit ? (
<div className={styles.select_product_dropdown}>
<Dropdown
placeholder="select product"
searchable={true}
items={getSearchItems()}
onChange={(productMeta) => {dropdownChangeHandler(productMeta);}}
onSearchInputChange={(e) => {setSearchText(e.text);}}
/>
</div>
) : (
<div className={styles.edit_product_title}>
<Input
type='text'
value={editProduct.name}
disabled={true}
/>
</div>
)}
<div className={styles.add_highlights_header}>
Add/Edit Highlights
</div>
{/* HIGHLIGHTS LIST */}
<div>
{highlightList?.map((highlight, index) => (
<div className={styles.highlight_list}>
<div>
{highlight}
</div>
<div className={styles.highlight_list_delete}>
<SvgIcTrash
className={styles.highlight_delete}
style={{
height: "24px",
width: "auto"
}}
color="#2E31BE"
onClick={() => {
setHighlightList((prevItem) => {
return [...prevItem.slice(0, index), ...prevItem.slice(index+1)]
})
}}
/>
</div>
</div>
))}
</div>
<div className={styles.highlight_input_ok}>
{/* HIGHLIGHT INPUT */}
<div className={styles.highlight_input_div}>
<Input
placeholder='add highlights'
style={{
padding: "0px"
}}
max={200}
min={1}
value={highlightInput}
disabled={ !isEdit && Object.keys(selectedDropdownProduct).length === 0 ? true : false }
onChange={(e) => {setHighlightInput(e.target.value)}}
onKeyPress={(e) => {
if (e.key === 'Enter') {
setHighlightList([...highlightList, highlightInput])
setHighlightInput("")
}
}}
/>
</div>
{/* HIGHLIGHT OK BUTTON */}
<div
onClick={() => {
setHighlightList([...highlightList, highlightInput])
setHighlightInput("")
}}
>
{highlightInput && (
<SvgIcConfirm
color='#2e31be'
style={{
height: "24px",
width: "auto",
cursor: "pointer"
}}
/>
)}
</div>
</div>
{/* ENABLE PRICE DROP CHECKBOX */}
<div className={styles.enable_price_drop_checkbox}>
<Checkbox
labelText="Enable 'Price Drop' label whenever price is reduced in last 2 days"
disabled={ !isEdit && Object.keys(selectedDropdownProduct).length === 0 ? true : false }
checkboxValue={checkboxValue}
onChange={(changedState) => {
setCheckboxValue(changedState);
}}
/>
</div>
</div>
<div className={styles.highlight_preview_box}>
<div className={styles.preview_box_header}>
Preview
</div>
<div className={styles.horizontal_line}></div>
<div>
{highlightList.length>0 && <div className={styles.highlightTitle}>Product Highlights</div>}
<div>
{highlightList?.map((highlight) => (
<div className={styles.highlightList}>{highlight}</div>
))}
</div>
</div>
</div>
</div>
</div>
</>
)
}
Step 3: Add CSS for the CreateHighlight component
-
In the
/app/src/views/style/directory create a new file calledcreateHighlight.module.css -
Add the following code to the file
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap');
.main_wrapper {
font-family: Inter;
position: relative;
box-sizing: border-box;
background: #fff;
border: 1px solid #f3f3f3;
border-radius: 12px;
padding: 24px;
margin: 24px;
}
.navbar {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.navbar_left_header {
display: flex;
align-items: center;
}
.main_title {
font-size: 22px;
font-weight: 600;
margin: 0 24px;
color: #41434c;
}
.back_arrow {
cursor: pointer;
}
.navbar_buttons {
display: flex;
flex-direction: row;
align-items: center;
}
.navbar_buttons Button {
margin: 0 16px;
}
/* content wrapper */
.content_wrapper {
display: flex;
flex-direction: row;
gap: 16px;
margin-top: 16px;
}
/* details box */
.highlight_detail_box {
flex: 1.5;
padding: 16px;
}
.highlight_detail_box_header {
font-size: 16px;
font-weight: 700;
color: #41434c;
}
/* DROP DOWN */
.select_product_dropdown {
margin: 12px 0;
max-width: 50%;
}
.edit_product_title {
margin: 12px 0;
max-width: 50%;
}
.edit_product_title div {
padding: 0;
max-height: 64px;
}
.edit_product_title input {
padding: 6px 24px;
}
.add_highlights_header {
font-size: 16px;
font-weight: 400;
line-height: 140%;
margin-top: 24px;
color: rgba(102, 102, 102, 0.5);
}
/* HIGHLIGHT LIST */
.highlight_list {
display: flex;
flex-direction: row;
padding: 8px 16px;
align-items: center;
justify-content: space-between;
gap: 12px;
font-weight: 400;
line-height: 140%;
color: #4f4f4f;
font-size: 14px;
}
.highlight_list_delete {
padding: 0 4px;
cursor: pointer;
}
/* HIGHLIGHT INPUT */
.highlight_input_ok {
margin: 12px 0;
display: flex;
flex-direction: row;
align-items: center;
gap: 24px;
}
.highlight_input_div {
min-width: 360px;
max-width: 512px;
width: -webkit-fill-available;
}
.highlight_input_div input {
font-size: 16px;
padding: 0px 16px;
}
.highlight_input_div input::placeholder {
font-size: 16px;
}
/* preview box */
.highlight_preview_box {
margin-top: 12px;
padding: 16px;
flex: 1;
border: 1px solid #e4e5e6;
border-radius: 4px;
}
.preview_box_header {
font-size: 16px;
font-weight: 700;
color: #41434c;
}
.horizontal_line {
border: 1px solid #ccc;
}
.highlightTitle {
font-size: 18px;
font-weight: 700;
color: #000;
padding: 16px 0 0 0;
font-family: 'Montserrat', sans-serif;
}
.highlightList {
font-size: 16px;
font-weight: 400;
color: #000;
padding: 10px 0;
font-family: 'Montserrat', sans-serif;
}
.enable_price_drop_checkbox {
margin: 24px 0px;
}
.enable_price_drop_checkbox label {
justify-content: flex-start;
}
Step 4: Create a Route for CreateHighlight component
-
Add the following objects to the
CreateBrowserRouterlist in/app/src/router/index.jsfile{
path: "/company/:company_id/:application_id/highlight/create",
element: <CreateHighlight />
},
{
path: "/company/:company_id/:application_id/highlight/:item_code",
element: <CreateHighlight />
} -
Import
ProductListcomponent at the top of/app/src/router/index.jsfileimport CreateHighlight from "../views/CreateHighlight";noteWe are using the same component
CreateHighlightfor creating new product highlights and updating existing product highlights.
Step 5: Restart the extension server and relaunch the extension
-
Clicking on this arrow button redirect the user to the CreateHighlight page


Now you can create Highlights for products by following the below steps:
- Select any product from the dropdown.
- Add highlights using an input text box.
- After adding all the highlights click on Submit.
![]()
It’ll redirect to the ProductList page
![]()
We’ve completed the UI flow of the Extension. Now in the next step, we will add functionality to show highlights on the Product description page of the store website.