Skip to main content

Tutorial: Product Highlights

You will build an illustrative Product Highlights extension in this tutorial in Node+React technology stack. You'll end up creating a working extension that would enable sellers to create highlights for required products and display the price drop tag under the price if it is altered recently.

tip

Before starting with this tutorial, make sure you have created extension using create extension guide.

Tutorial is divided into following sections

  1. Adding DB Configuration
  2. Building the extension home page
  3. Building 'Highlights Listing' page
  4. Building 'Create Highlight' page
  5. Injecting Javascript into the storefront theme
  6. Configuring Webhook

What you will learn

  1. How to call Fynd Platform SDK methods
  2. Build Extension UI using the Nitrozen design system
  3. Create script tag binding to inject script tag in storefront to render extension UI elements within storefront theme
  4. Configure Webhooks for listening to required events

Download the Product Highlight example extension

To view the complete code used for the Product Highlight example extension, you can download the source code from Github. To learn about the code step-by-step, we recommend you to follow this tutorial series.

Adding DB Configuration

Step 1: Populate test products in your development account

Login to FDK-CLI using command below:

note

If logging in the previous step has been completed, there is no need to log in again. This step can be skipped.

fdk login

Verify if you are logged in or not by running the following command:

fdk user

Run the following command and provide company id of required development account to populate test products in it.

fdk populate

Step 2: Provide MongoDB configuration

Add port and MongoDB local URL in /.env file

MONGODB_URI="mongodb://localhost:27017/productHighlights"
PORT=8080

Add MongoDB URL in /app/config.js file

mongodb: {
uri: {
doc: "Mongodb uri",
default: "",
env: 'MONGODB_URI'
}
},

Step 3: Preview the changes

Preview the changes using these steps

Building the extension home page

In this section, you'll use Node.js, React, MongoDB, and Nitrozen components to build your Extension home page.

This section consists of three steps:

  • Create schema for saving product and its highlights using Mongoose library.
  • Make changes in the Extension backend.
  • Add additional components to the Home page of the Extension Frontend.

Step 1: Create MongoDB schema

The app needs a database to store the Product details so that merchants can view and edit the saved product highlights.

The database collection includes the product highlights, as well as the basic details about the product such as name, product brand, product slug, and product item code.

  • Install the mongoose package using the following command

    npm i mongoose
  • Create new /app/db/mongo.js file and add the following code to the file

    const config = require('../config');
    const mongoose = require('mongoose');

    // mongodb connection
    mongoose.connect(config.mongodb.uri);

    const ProductSchema = new mongoose.Schema({
    name: {
    type: String,
    },
    image: {
    type: String
    },
    brand_name: {
    type: String
    },
    category_slug: {
    type: String
    },
    highlights: {
    type: [String]
    },
    price: {
    type: Object
    },
    enablePriceDrop: {
    type: Boolean,
    default: false
    }
    })

    const ProductHighlightSchema = new mongoose.Schema({
    company_id: {
    type: String,
    },
    application_id: {
    type: String,
    },
    product_item_code: {
    type: Number,
    unique: true,
    index: true
    },
    product_slug: {
    type: String,
    unique: true,
    index: true
    },
    product: {
    type: ProductSchema
    },
    is_active: {
    type: Boolean,
    default: false
    }
    })

    const ProductHighlightRecord = mongoose.model("productHighlight", ProductHighlightSchema);

    module.exports = { ProductHighlightRecord }

Step 2: Update the backend API

  • Change the name of /app/routes/v1.router.js file to /app/routes/product.router.js and make changes to the GET /applications API

    const { ProductHighlightRecord } = require('../db/mongo')

    // Get applications list
    router.get('/applications', async function view(req, res, next) {
    try {
    const {
    platformClient
    } = req;
    const companyId = req.fdkSession.company_id;

    let applications = await platformClient.configuration.getApplications({
    pageSize: 1000,
    q: JSON.stringify({"is_active": true})
    })

    let activeApplicationSet = new Set();
    let productSchema = await ProductHighlightRecord.find({company_id: companyId, is_active: true}, {application_id: 1});
    for (let index = 0; index < productSchema.length; index++) {
    activeApplicationSet.add(productSchema[index].application_id.toString());
    }
    for (let index = 0; index < applications?.items?.length; index++) {
    applications.items[index] = {
    ...applications?.items?.[index],
    is_active: activeApplicationSet.has(applications.items[index]._id.toString()) ? true : false
    }
    }
    return res.status(200).json(applications);

    } catch (err) {
    next(err);
    }
    });
  • Also made the following changes to the /app/server.js file

    change lines 3 and 31 from

    3.    const v1Router = require("./routes/v1.router");
    31. apiRoutes.use('/v1.0', v1Router)

    to

    3.    const productRouter = require("./routes/product.router");
    31. apiRoutes.use('/v1.0', productRouter)

Step 3: Changes in the Front End

We’ll be using the Badge and Input components and SvgIcArrowNext Icon of the Nitrozen library

tip

Check out Nitrozen’s storybook for detailed documentation and usage instructions.

  1. Open /src/views/Home.jsx
  2. Delete the contents of the file.
  3. Add the following code:
import React, { useState, useEffect } from "react";
import { useNavigate, useParams } from 'react-router-dom'

import "./style/home.css";
import Loader from "../components/Loader";
import { Badge, SvgIcArrowNext, Input } from "@gofynd/nitrozen-react";
import MainService from "../services/main-service";

export default function Home() {
const [pageLoading, setPageLoading] = useState(false);
const [applicationList, setApplicationList] = useState([]);
const [allApplications, setAllApplications] = useState([]);

const navigate = useNavigate();
const { company_id } = useParams();

useEffect(() => {
fetchApplications();
}, []);

const fetchApplications = async () => {
setPageLoading(true);
try {
const { data } = await MainService.getAllApplications();
setAllApplications(data.items);
const temp = data.items.map((ele) => {
ele.text = ele.name;
ele.value = ele._id;
ele.image = ele.logo;
ele.logo = ele.image && ele.image.secure_url;
return ele;
});
setApplicationList(temp);
setPageLoading(false);
} catch (e) {
setPageLoading(false);
}
};

function searchApplication(event) {
let searchText = event.target.value;
if (!searchText) {
setApplicationList(allApplications.map((app) => app));
} else {
setApplicationList(
allApplications.filter((item) => {
return item.name.toLowerCase().includes(searchText.toLowerCase());
})
);
}
}

function clickOnSalesChannel(application_id) {
navigate(`/company/${company_id}/${application_id}/product-list`)
}

return (
<>
{pageLoading ? (
<Loader />
) : (
<div className="application-container">

<div className="saleschannel-title">Sales Channel</div>

<div className="search-box">
<Input
showSearchIcon
placeholder='search sales channels'
disabled={ Object.keys(allApplications).length === 0 ? true : false }
onChange={searchApplication}
/>
</div>

<div className="sales-channels-container">
{applicationList.map((application) => {
return (
<div className="app-box">

<div className="logo">
<img src={application.logo ? application.logo : "https://platform.fynd.com/public/admin/assets/pngs/fynd-store.png"} alt="logo" />
</div>

<div className="line-1">{application.name}</div>

<div className="line-2">{application.domain.name}</div>

<div className="button-and-arrow">
<div>
<Badge
fill
kind="normal"
state={application.is_active ? "success" : "disable"}
labelText={application.is_active ? "ACTIVE" : "INACTIVE"}
style={{
padding: "10px 6px"
}}
/>
</div>

<div className="card-arrow">
<div className="card-arrow-box"
onClick={() => clickOnSalesChannel(application._id)}
>
<SvgIcArrowNext
className="arrow-next"
/>
</div>
</div>

</div>

</div>
);
})}
{applicationList.length % 3 === 2 && (
<div className="app-box hidden"></div>
)}
</div>

</div>
)}
</>
);
}

Now, update the CSS file for the above code

  1. Open /src/views/style/home.css
  2. Delete the contents of the file.
  3. Add the following code:
caution

CSS written in this file will have an impact on the entire project.

html {
height: 100%;
width: 100%;
font-size: 8px;
}

body {
margin: 0;
font-family: Inter;
background-color: #f8f8f8 !important;
width: 100%;
height: 100%;
@media @mobile {
-webkit-tap-highlight-color: transparent;
}
}

.application-container {
font-family: Inter;
position: relative;
box-sizing: border-box;
background: #fff;
border: 1px solid #f3f3f3;
border-radius: 12px;
padding: 24px;
margin: 24px;
}

.saleschannel-title {
font-weight: 700;
font-size: 20px;
margin-bottom: 8px;
}

.search-box {
margin-top: 20px;
}

.sales-channels-container {
display: grid;
grid-template-columns: 25% 25% 25% 25%;
grid-column-gap: 18px;
grid-row-gap: 18px;
margin-top: 20px;
width: calc(100% - 54px);
}

.app-box {
background-color: #ffffff;
border: 1px solid #e4e5e6;
padding: 20px;
border-radius: 12px;
}

.app-box .logo {
width: 48px;
height: 48px;
}

.app-box .logo img {
width: 100%;
height: auto;
}

.app-box .line-1 {
font-weight: 600;
font-size: 16px;
line-height: 26px;
margin-top: 20px;
}

.app-box .line-2 {
color: #9b9b9b;
line-height: 22px;
font-size: 12px;
}

.app-box + .app-box:nth-child(3n + 1) {
margin-left: 0;
}

/* card footer elements */
.app-box .button-and-arrow {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 40px;
}

.card-arrow-box {
border: 1px solid #2E31BE;
border-radius: 4px;
height: 36px;
width: 36px;
display: flex;
align-items: center;
justify-content: center;
}

.card-arrow-box:hover {
transition-duration: 0.4s;
background-color: #2E31BE;
cursor: pointer;
}

.arrow-next {
color: #2E31BE;
width: 16px;
height: auto;
}

.card-arrow-box:hover .arrow-next {
transition-duration: 0.4s;
color: #ffffff;
cursor: pointer;
}

.hidden {
visibility: hidden;
}

Step 4: Restart the server and refresh extension page in development account

Extension home page

Building 'Highlights Listing' page

Step 1: Create Backend APIs

  • Add the following code to the /app/routes/product.route.js.

    // get product highlights list
    router.get("/:application_id/highlight/list", async function view(req, res, next) {
    try {
    const { application_id } = req.params;
    const companyId = Number(req.fdkSession.company_id);

    let data = await ProductHighlightRecord.find({application_id: application_id, company_id: companyId}).exec();
    return res.status(200).json(data);
    } catch(error) {
    next(error)
    }
    })

    // get product highlight by product item code or slug
    router.get("/:application_id/highlight", async function view(req, res, next) {
    try {
    const { application_id } = req.params;
    let { slug, item_code } = req.query;
    const companyId = Number(req.fdkSession.company_id);
    // const companyId = 11197;
    let data;

    if (item_code) {
    item_code = Number(item_code)
    data = await ProductHighlightRecord.findOne({
    application_id: application_id, company_id: companyId, product_item_code: item_code
    }).exec();
    } else if (slug) {
    data = await ProductHighlightRecord.findOne({
    company_id: companyId, application_id: application_id, product_slug: slug
    })
    } else {
    return res.status(400).json({"Error": "Invalid Item code or slug in the query param"});
    }

    return res.status(200).json(data);
    } catch(error) {
    next(error);
    }
    })

    // delete product highlight by item code or slug
    router.delete("/:application_id/highlight", async function view(req, res, next) {
    try {
    const { application_id } = req.params;
    const { slug } = req.query;
    const item_code = Number(req.query.item_code);
    const companyId = Number(req.fdkSession.company_id);
    let data;

    if (item_code !== "") {
    data = await ProductHighlightRecord.deleteOne({
    application_id: application_id, company_id: companyId, product_item_code: item_code
    }).exec();
    } else if (slug !== "") {
    data = await ProductHighlightRecord.deleteOne({
    company_id: companyId, application_id: application_id, product_slug: slug
    }).exec();
    } else {
    throw new Error("Invalid Item code or slug in query param");
    }

    return res.status(200).json(data);
    } catch(error) {
    next(error);
    }
    })
  • Also, add the following endpoints to the Endpoints object in /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 /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 /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 /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 /src/router/index.js file

    {
    path: "/company/:company_id/:application_id/product-list/",
    element: <ProductList />
    },
  • Import ProductList component at the top of /src/router/index.js file

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

Step 5: Restart the extension server and refresh extension page in development account

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.

Building 'Create Highlight' page

Step 1: Create Backend APIs

  • Add the following code to the /app/routes/product.route.js

    // get product list
    router.get('/:application_id/products', async function view(req, res, next) {
    try {
    const { platformClient } = req;
    const { application_id } = req.params;
    const { query } = req.query;
    let response = await platformClient.application(application_id).catalog.getAppProducts({
    pageNo: 1,
    pageSize: 10,
    q: query
    })
    return res.status(200).json(response)
    } catch(error) {
    next(error);
    }
    })

    // update product highlights
    router.post("/:application_id/product/:item_id/highlights", async function view(req, res, next) {
    try {
    const { application_id, item_id } = req.params;
    const { product_meta, highlights, enablePriceDrop } = req.body
    const companyId = Number(req.fdkSession.company_id);

    let productHighlightRecord = await ProductHighlightRecord.findOne({
    company_id: companyId, application_id: application_id, product_item_code: item_id
    }).exec()

    if (productHighlightRecord) {
    productHighlightRecord.product.highlights = highlights;
    productHighlightRecord.product.enablePriceDrop = enablePriceDrop;
    await productHighlightRecord.save();

    } else {
    const prepareRecord = {
    company_id: companyId,
    application_id: application_id,
    product_item_code: product_meta.product_item_code,
    product_slug: product_meta.product_slug,
    product: {
    name: product_meta.name,
    image: product_meta.image,
    brand_name: product_meta.brand_name,
    category_slug: product_meta.category_slug,
    highlights: highlights,
    price: product_meta.price,
    enablePriceDrop: enablePriceDrop
    },
    is_active: false
    }
    productHighlightRecord = await new ProductHighlightRecord(prepareRecord).save();
    }
    return res.status(200).json(productHighlightRecord);

    } catch(error) {
    next(error);
    }
    })
  • Also, add the following endpoints to the Endpoints object in /src/service/endpoint.service.js file

    GET_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 /src/service/main-service.js file

    getAllProducts(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 /src/views/ directory create a new file called CreateHighlight.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 /src/views/style/ directory create a new file called createHighlight.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 /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 /src/router/index.js file

    import CreateHighlight from "../views/CreateHighlight";
    note

    We are using the same component CreateHighlight for creating new product highlights and updating existing product highlights.

Step 5: Restart the extension server and refresh extension page in development account

  • Clicking on this arrow button redirect the user to the CreateHighlight page

    click on create product highlight button

    create product highlight page

Now you can create Highlights for products by following the below steps:

  1. Select any product from the dropdown.
  2. Add highlights using an input text box.
  3. After adding all the highlights click on Submit.

submit product highlight

It’ll redirect to the ProductList page

product listing 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.

Injecting Javascript into the storefront theme

We’ll need to inject Javascript into the theme to show the product highlights and price drop label. Fynd Platform Theme engine is built with Vue2, that’s why we will have to create an Injectable script in Vue itself.

info

Fynd Platform now supports React themes as well. For more information on theme development, please refer to the documentation.

Step 1: Create Binding in Extension

  1. Visit Fynd Partners.
  2. Click on Manage and Select your organization.
  3. Click on Extensions from the left Menu.
  4. Select your Extension.
  5. Click on three dots and then select Bindings as shown in the below image.

click on bindings

  1. Click on Add Template and fill in the data as shown in the below image.

create binding for product highlights

  1. Repeat Step 6 for the Price Drop Tag also.

create binding for price drop

binding list page

Now in the next step, we’ll create Vue components and inject them using these bindings.

Step 2: Setup Vue project

  • Install vue-cli npm package using the following command

    npm i -g @vue/cli
  • Create new directory binding inside your project root directory using the following command

    mkdir bindings
    cd bindings
  • Create Vue project

    vue create .
    note

    Choose Yes for the prompt “Generate project in the current directory?” and select Vue2 for the prompt “Please pick a preset:”

  • Open /bindings/vue.config.js file and replace the code with the following code

    module.exports = {
    pages: {
    index: {
    // entry for the page
    entry: process.env.NODE_ENV == "development" ? 'src/dev.js' : 'src/main.js'
    },
    },
    devServer: {
    disableHostCheck: true
    }
    }
  • Install axios and url-join package inside /bindings/ directory using the following commands

    npm i axios
    npm i url-join
  • In the /binding/ directory, open the /package.json file and replace the following line

    "build": "vue-cli-service build",

    with

    "build": "vue-cli-service build --target lib src/main.js --name product-highlights",
  • Delete unnecessary files such as README.md and jsconfig.json files from the /bindings/ directory

  • Delete all the files and folders from the /bindings/src directory

  • In the /bindings/src/ directory, create a new file called dev.js and add the following code to the file:

    import App from './App.vue';
    import Vue from 'vue';

    new Vue({
    render: h => h(App)
    }).$mount('#app')
  • In the /bindings/src/ directory, create a new file called main.js and add the following code to the file:

    import Highlights from './Highlights.vue'
    import PriceDrop from './PriceDrop.vue'

    window.FPI.extension.register("#product-highlights", {
    mounted(element) {
    window.FPI.extension.mountApp({
    element,
    component: Highlights
    });
    }
    })

    window.FPI.extension.register("#product-price-drop", {
    mounted(element) {
    window.FPI.extension.mountApp({
    element,
    component: PriceDrop
    })
    }
    })

Step 3: Create Vue components

  • In the /bindings/src/ directory, create a new file called Highlights.vue and add the following code to the file:

    <template>
    <div class="product-highlights">
    <div v-if="highlightsData">
    <div class="highlightTitle">Product Highlights</div>
    <div
    v-for="(highlight, index) in highlightsData"
    :key="index"
    >
    <div class="highlightList">{{highlight}}</div>
    </div>
    </div>
    </div>
    </template>

    <script>
    import axios from 'axios';
    import urlJoin from "url-join";

    export default {
    name: "ProductHighlights",

    data() {
    return {
    highlightsData: null
    };
    },

    async mounted() {
    const baseURL = window.location.origin;
    const product_slug = this.$route.params.slug;


    let { data } = await axios.get(
    urlJoin(baseURL, 'ext/producthighlights/highlight'),
    { params: {slug: product_slug} }
    );

    if (data && data.is_active && data.product && data.product.highlights && data.product.highlights.length) {
    this.highlightsData = data.product.highlights
    }
    },
    }
    </script>

    <style>
    .highlightTitle {
    font-size: 14px;
    font-weight: 700;
    color: #000000;
    padding: 32px 0 6px 0;
    }

    .highlightList {
    font-size: 14px;
    font-weight: 400;
    color: #000000;
    padding: 8px 0;
    }
    </style>
  • Download drop-price-tag.svg file from GitHub and add it into the /bindings/assets/ directory. Here is the link to SVG : Click Here

  • In the /bindings/src/ directory, create a new file called PriceDrop.vue and add the following code to the file:

    <template>
    <div>
    <div v-if="showPriceDrop">
    <img src="../assets/drop-price-tag.svg" alt="price-drop-png" />
    </div>
    </div>
    </template>

    <script>
    import axios from "axios";
    import urlJoin from "url-join";
    export default {
    name: 'PriceDrop',

    data() {
    return {
    showPriceDrop: false
    }
    },

    async mounted() {
    const baseURL = window.location.origin;
    const product_slug = this.$route.params.slug;

    let { data } = await axios.get(
    urlJoin(baseURL, 'ext/producthighlights/price-drop'),
    { params: {slug: product_slug} }
    );

    if (data && data.showPriceDrop) {
    this.showPriceDrop = true;
    }
    }

    }
    </script>

    <style scoped>
    img {
    height: 48px;
    width: auto;
    }
    </style>

  • Build the project using the following command inside /bindings/ directory.

    npm run build

Step 4: Write logic for script injection

  • In the /app/ directory, open the /server.js file and replace following line

    app.use(express.static(path.resolve(__dirname, "../build/")));

    with

    app.use(express.static(path.resolve(__dirname, "../build/")));
    app.use('/bindings/product-highlights', express.static(path.join(__dirname, '../bindings/dist')))

    with these we are serving min.js files which we have created by building the bindings bundle. Now these min.js files can be accessed using Extension base URL.

  • To maintain the record of the Proxy we will create one MongoDB schema. for that open the /app/db/mongo.js file and add the following code

    const ProxySchema = new mongoose.Schema({
    company_id: {
    type: String,
    required: true
    },
    application_id: {
    type: String,
    required: true
    },
    attached_path: {
    type: String
    },
    proxy_url:{
    type: String
    },
    })
    const ProxyRecord = mongoose.model("Proxy", ProxySchema);
  • add also export newly created ProxyRecord at end of the file

    module.exports = { ProductHighlightRecord, ProxyRecord }
  • For adding a Proxy we’ll require unique string which will be passed as a parameter to the addProxyPath method.

  • Add the following line at the end of .env file at project directory

    PROXY_ATTACH_PATH="producthighlights"
  • Add proxy_attach_path to /app/config.js file

    proxy_attach_path: {
    doc: "Proxy attach path",
    default: '',
    env: "PROXY_ATTACH_PATH"
    },
  • In the /app/routes/ directory, create a new file called script.router.js and add the following code to the file:

    const express = require('express');
    const router = express.Router();
    const config = require('../config');
    const { ProductHighlightRecord, ProxyRecord } = require('../db/mongo')

    function getTagSchema(application_id) {
    return [
    {
    "name": "Product Highlights injection script",
    "sub_type": "external",
    "type": "js",
    "position": "body-bottom",
    "url": `${config.extension.base_url}/bindings/product-highlights/product-highlights.umd.js`,
    "attributes": {
    "id": application_id
    }
    },
    {
    "name": "Product Highlights injection css",
    "sub_type": "external",
    "type": "css",
    "position": "head",
    "url": `${config.extension.base_url}/bindings/product-highlights/product-highlights.css`,
    "attributes": {
    }
    }
    ]
    }

    router.post("/:application_id/tag/:item_code", async function view(req, res, next) {
    try {
    const { platformClient } = req;
    const { application_id, item_code } = req.params;
    const companyId = Number(req.fdkSession.company_id);

    let response;
    let data = await ProductHighlightRecord.findOne({
    application_id: application_id, company_id: companyId, product_item_code: item_code
    }).exec()

    if (data) {
    data.is_active = true;
    await data.save();

    // add proxy
    let proxyCount = await ProxyRecord.find({company_id: companyId, application_id: application_id}).count();