ecoseller logo ecoseller

Table of contents:

Dashboard & Storefront

The dashboard is a part of the application that is used by the administrators to manage the application. The storefront on the other hand, is a part of the application that is used by the customers to browse and buy products. Both of these parts are standalone applications built using Next.js framework and are served by the same server. Their directory structure consists of various parts, among which the most important are:

Context providers

To be able to access various data in different parts of the application, we use React Context. More information about React Context can be found on the following links:

In further parts of this section, we assume that the reader is familiar with React Context and its usage from the links above.

In ecoseller, we use various context providers, and now we will describe them in more detail.

UserProvider

UserProvider is a context provider that provides information about the currently logged in user to its children. It is used in both Dashboard and Storefront component, although they differ a bit in data they provide.

Parameters

Return value

UserProvider in Storefront only provides user data, while Dashboard provides both user and roles data.

Usage example

UserProvider already wraps whole application in both Dashboard and Storefront components, so we can access user data in any child component. To access user data, we use useUser hook:

PermissionProvider

As mentioned in Authorization section, ecoseller uses roles and permissions to restrict access to certain parts of the application. PermissionProvider is a context provider that provides information about user’s permissions to its children. It is used in Dashboard component.

To ensure proper usage, we defined ContextPermissions type with permissions that may be passed to the provider. The type is defined in dashboard/utils/context/permission.tsx file.

Parameters

Return value

Usage example

To check whether the user has user_add_permission permission for adding new user, wrap the component with PermissionProvider:

    <PermissionProvider allowedPermissions={["user_add_permission"]}>
        <EditableContentWrapper>
            <CreateUser />
        </EditableContentWrapper>
    </PermissionProvider>

Now, we can check in respective component whether the user has the permission:

    const CreateUser () => {
        ...
        const { hasPermission } = usePermission();
        ...
        return (
            ...
            <TextField
                disabled={!hasPermission}
            >
            Email
            </TextField>
            ...
        );
    };
const EditableContentWrapper = () => {
    ...
    const { hasPermission } = usePermission();
    ...
    return (
        ...
        <Button
            disabled={!hasPermission}
        >
        Save
        </Button>
        ...
    );
};

This will disable the TextField and Button components if the user does not have user_add_permission permission.

CartProvider

CartProvider is a context provider that provides information about the user’s cart as well as some usefull functions to its children. It is used only in Storefront component.

Parameters

Return value

Functions provided by CartProvider

addToCart

Adds item to the cart. If the item is already in the cart, it updates its quantity. Takes following parameters:

removeFromCart

Deletes item from the cart. Takes following parameters:

updateQuantity

Updates quantity of the item in the cart. Takes following parameters:

clearCart

Clears the cart. Takes no parameters.

cartProductQuantity

Returns quantity of the product in the cart. Takes following parameters:

Usage example

CartProvider already wraps whole application, so we can access data or functions in any child component. To do so, we use useCart hook:

const ChildComponent = () => {
    ...
    const { cart, cartSize, addToCart, removeFromCart, updateQuantity, clearCart, cartProductQuantity } = useCart();
    ...
    return (
        ...
    );
};

CookieProvider

CookieProvider is a context provider that provides information about the user’s cookies as well as some usefull functions to its children. It is used only in Storefront component.

Parameters

Return value

Functions provided by CookieProvider

setCookieState

Sets cookie state. Takes following parameters:

setCookieSettingToCookies

Sets cookie setting to cookies. Takes following parameters:

toggleDisclaimer

Toggles cookie disclaimer. Takes following parameters:

Usage example

CookieProvider already wraps whole application, so we can access data or functions in any child component. To do so, we use useCookie hook:

const ChildComponent = () => {
    ...
    const { cookieState, setCookieState, setCookieSettingToCookies, toggleDisclaimer } = useCookie();
    ...
    return (
        ...
    );
};

CountryProvider

CountryProvider is a context provider that provides information about the country that is currently set by the user, as well as some usefull functions to its children. It is used only in Storefront component.

Parameters

Return value

Functions provided by CountryProvider

setCountryCookieAndLocale

Sets country cookie and locale. Takes following parameters:

Usage example

CountryProvider already wraps whole application, so we can access data or functions in any child component. To do so, we use useCountry hook:

const ChildComponent = () => {
    ...
    const { country, countryList, setCountryCookieAndLocale } = useCountry();
    ...
    return (
        ...
    );
};

RecommenderProvider

RecommenderProvider is a context provider that provides information about the user’s recommender session as well as some usefull functions to send either recommender event or retrive recommendations. It is used only in Storefront component. This context provider creates a new recommender session for each user (if it does not exist yet) and stores it in the cookie storage under rsSession key.

Parameters

Recommender events

Recommender events are used to track user’s behaviour on the website. They are sent to the recommender server and are used to generate recommendations. They are defined in RS_EVENT variablle. Each event has its own payload. More information about recommender events can be found here

Recommender situations

Recommender situations are used to define the context in which the user is currently in. They are defined in RS_RECOMMENDATIONS_SITUATIONS variable. Each situation has its own payload.

Return value

Functions provided by RecommenderProvider

User does not have to send session id directly, it’s injected to each request automatically.

sendEvent

Sends recommender event. For example it can be information about adding product to the cart, or leaving product page. Takes following parameters:

getRecommendations

Gets recommendations for given situation. It returns recommmended products for the given session. Takes following parameters:

Usage example

RecommenderProvider already wraps whole application, so we can access data or functions in any child component. To do so, we use useRecommender hook:

const ChildComponent = () => {
    ...
    const { session, sendEvent, getRecommendations } = useRecommender();
    ...
    return (
        ...
    );
};

Interceptors

Interceptors are used to intercept requests and responses before they are handled by the application. In ecoseller, we use them to add authorization token and other data to requests and to handle errors. We use axios library for handling requests and responses. More information about interceptors can be found on the following links:

In further parts of this section, we assume that the reader is familiar with axios library and its usage from the links above. Interceptors in the Dashboard and Storefront differ a bit, so we will describe them separately.

Request interceptor - Dashboard

In the Dashboard, we use request interceptor to add authorization token to requests. The interceptor is defined in dashboard/utils/interceptors/api.ts file. Fisrtly, we define api axios instance with base url and headers:

export const api = axios.create({
  baseURL,
  headers: {
    "Content-Type": "application/json",
  },
  withCredentials: true,
});

Then, we add request interceptor to the api instance:

Request interceptor - Storefront

In the Storefront, we use request interceptor to add authorization token and country locale to requests. The interceptor is defined in storefront/utils/interceptors/api.ts file. Axios instance in the Storefront is defined similarly as in the Dashboard (see above). The difference is in the request interceptor, where we also add a country locale to the Accept-Language header:

api.interceptors.request.use((config) => {

  ... // similar to the Dashboard

  // set locale (if present)
  const locale = getLocale();
  if (locale) {
    config.headers["Accept-Language"] = locale;
  }

  return config;
});

API Routes

Next.js API Routes provide a convenient way to create server-side endpoints within your Next.js application. These API routes allow you to handle server-side logic and expose custom API endpoints. This section of the documentation explores the usage of Next.js API Routes in the context of ecoseller.

In the Ecoseller architecture, it is recommended to avoid exposing the backend service directly to the client. By using Next.js API Routes, you can encapsulate server-side logic within your Next.js application, ensuring a secure and controlled environment for handling data and performing server-side operations. This approach provides several benefits:

This approach also helped us with cookie handling and token refreshing. Setting cookie client side caused some problems with refreshing tokens, so we decided to set cookies in API routes instead.

API Routes in the Dashboard

All API routes can be found in the dashboard/pages/api directory. All those routes use simmilar logic and send requests to the backend via api interceptor.

export const productDetailAPI = async (
  method: HTTPMETHOD,
  id: number,
  req?: NextApiRequest,
  res?: NextApiResponse
) => {
  if (req && res) {
    setRequestResponse(req, res);
  }

  const url = `/product/dashboard/detail/${id}/`;

  switch (method) {
    case "GET":
      return await api
        .get(url)
        .then((response) => response.data)
        .then((data: IProduct) => {
          return data;
        })
        .catch((error: any) => {
          throw error;
        });
    case "DELETE":
      return await api
        .delete(url)
        .then((response) => response.data)
        .then((data: IProduct) => {
          return data;
        })
        .catch((error: any) => {
          throw error;
        });
    case "PUT":
      const body = req?.body;
      if (!body) throw new Error("Body is empty");
      return await api
        .put(url, body)
        .then((response) => response.data)
        .then((data: IProduct) => {
          return data;
        })
        .catch((error: any) => {
          throw error;
        });

    default:
      throw new Error("Method not supported");
  }
};

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  /**
   * This is a wrapper for the product detail api in the backend
   */

  const { id } = req.query;
  const { method } = req;

  if (!id) return res.status(400).json({ message: "id is required" });
  if (Array.isArray(id) || isNaN(Number(id)))
    return res.status(400).json({ message: "id must be a number" });

  if (method === "GET" || method === "PUT" || method === "DELETE") {
    return productDetailAPI(method, Number(id), req, res)
      .then((data) => res.status(200).json(data))
      .catch((error) => res.status(400).json(null));
  } else {
    return res.status(400).json({ message: "Method not supported" });
  }
};

export default handler;

API Routes in the Storefront

All API routes can be found in the storefront/pages/api directory. All those routes use simmilar logic and send requests to the backend via api interceptor.

export const cartAPI = async (
  method: HTTPMETHOD,
  req: NextApiRequest,
  res: NextApiResponse
) => {
  if (req && res) {
    setRequestResponse(req, res);
  }

  const url = `/cart/storefront/`;

  switch (method) {
    case "POST":
      return await api
        .post(url, req.body)
        .then((response) => response.data)
        .then((data) => {
          return data;
        })
        .catch((error: any) => {
          throw error;
        });
    default:
      throw new Error("Method not supported");
  }
};

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  const { method } = req;

  if (method == "POST") {
    return cartAPI("POST", req, res)
      .then((data) => res.status(201).json(data))
      .catch((error) => res.status(400).json(null));
  }
  return res.status(404).json({ message: "Method not supported" });
};

export default handler;