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.java
file 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.java
file 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
Endpoints
object in/app/src/service/endpoint.service.js
fileGET_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
MainService
object in/app/src/service/main-service.js
filegetAllProducts(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
CreateBrowserRouter
list in/app/src/router/index.js
file{
path: "/company/:company_id/:application_id/highlight/create",
element: <CreateHighlight />
},
{
path: "/company/:company_id/:application_id/highlight/:item_code",
element: <CreateHighlight />
} -
Import
ProductList
component at the top of/app/src/router/index.js
fileimport CreateHighlight from "../views/CreateHighlight";
noteWe are using the same component
CreateHighlight
for 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.