You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
homepage/docs/widgets/authoring/guide.md

11 KiB

title description
Widget Guide How to create a custom widget for Homepage.

In this guide, we'll walk through the process of creating a custom widget for Homepage. We'll cover the basic structure of a widget, how to use translations, and how to fetch data from an API. By the end of this guide, you'll have a solid understanding of how to build your own custom widget.

Prerequisites:

  • Basic knowledge of React and JavaScript
  • Familiarity with the Homepage platform
  • Understanding of JSON and API interactions

Throughout this guide, we'll use yourwidget as a placeholder for the unique name of your custom widget. Replace yourwidget with the actual name of your widget. It should contain only lowercase letters and no spaces.

This guide makes use of a fake API, which would return a JSON response as such, when called with the v1/info endpoint:

{ "key1": 123, "key2": 456, "key3": 789 }

Set up the widget definition

Create a new folder for your widget in the src/widgets directory. Name the folder yourwidget.

Inside the yourwidget folder, create a new file named widget.js. This file will contain the metadata for your widget.

Open the widget.js file and add the following code:

import genericProxyHandler from "utils/proxy/handlers/generic"; // (1)!

const widget = /* (2)! */ {
  api: "{url}/{endpoint}" /* (3)! */,
  proxyHandler: genericProxyHandler /* (1)! */,

  mappings: /* (4)! */ {
    info: /* (5)! */ {
      endpoint: "v1/info" /* (6)! */,
    },
  },
};

export default widget;
  1. We import the genericProxyHandler from the utils/proxy/handlers/generic module. The genericProxyHandler is a generic handler that can be used to fetch data from an API. We then assign the genericProxyHandler to the proxyHandler property of the widget object. There are other handlers available that you can use depending on your requirements. You can also create custom handlers if needed.
  2. We define a widget object that contains the metadata for the widget.
  3. The API endpoint to fetch data from. You can use placeholders like {url} and {endpoint} to dynamically generate the API endpoint based on the widget configuration.
  4. An object that contains mappings for different endpoints. Each mapping should have an endpoint property that specifies the endpoint to fetch data from.
  5. A mapping named info that specifies the v1/info endpoint to fetch data from. This would be called from the component as such: #!js useWidgetAPI(widget, "info");
  6. The endpoint property of the info mapping specifies the endpoint to fetch data from. There are other properties you can pass to the mapping, such as method, headers, and body.

!!! warning "Important"

All widgets that fetch data from dynamic endpoints should have either `mappings` or an `allowedEndpoints` property.

Set up translation strings

Homepage uses translated and localized strings for all text and numerical content in widgets. English is the default language, and other languages can be added via Crowdin. To add the English translations for your widget, follow these steps:

Open the public/locales/en/common.js file.

Add a new object for your widget to the bottom of the list, like this:

"yourwidget": {
  "key1": "Value 1",
  "key2": "Value 2",
  "key3": "Value 3"
}

!!! note

Even if you nativly speak another language, you should only add English translations.  You can then add translations in your native language via [Crowdin](https://crowdin.com/project/gethomepage), once your widget is merged.

Create the widget component

Create a new file for your widgets component, named component.jsx, in the src/widgets/yourwidget directory. We'll build the contents of the component.jsx file step by step.

First, we'll import the necessary dependencies:

import { useTranslation } from "next-i18next"; // (1)!

import Container from "components/services/widget/container"; // (2)!
import Block from "components/services/widget/block"; // (3)!
import useWidgetAPI from "utils/proxy/use-widget-api"; // (4)!
  1. #!js useTranslation() is a hook provided by next-i18next that allows us to access the translation strings
  2. #!jsx <Container> and #!jsx <Block> are custom components that we'll use to structure our widget.
  3. #!jsx <Container> and #!jsx <Block> are custom components that we'll use to structure our widget.
  4. #!js useWidgetAPI(widget, endpoint) is a custom hook that we'll use to fetch data from an API.

Next, we'll define a functional component named Component that takes a service prop.

export default function Component({ service }) {}

We grab the helper functions from the useTranslation hook.

const { t } = useTranslation();

We destructure the widget object from the service prop. The widget object contains the metadata for the widget, such as the API endpoint to fetch data from.

const { widget } = service;

Now, the fun part! We use the useWidgetAPI hook to fetch data from an API. The useWidgetAPI hook takes two arguments: the widget object and the API endpoint to fetch data from. The useWidgetAPI hook returns an object with data and error properties.

const { data, error } = useWidgetAPI(widget, "info");

!!! tip "API Tips"

You'll see here how part of the API url is built using the `url` and `endpoint` properties from the widget definition.

In this case, we're fetching data from the `info` endpoint.  The `info` endpoint is defined in the `mappings` object.  So the full API endpoint will be `"{url}/v1/info"`.

The mapping and endpoint are often the same, but must be defined regardless.

Next, we check if there's an error or no data.

If there's an error, we return a Container and pass it the service and error as props. The Container component will handle displaying the error message.

if (error) {
  return <Container service={service} error={error} />;
}

If there's no data, we return a Container component with three Block components, each with a label.

if (!data) {
  return (
    <Container service={service}>
      <Block label="yourwidget.key1" />
      <Block label="yourwidget.key2" />
      <Block label="yourwidget.key3" />
    </Container>
  );
}

This will render the widget with placeholders for the data, i.e., a skeleton view.

!!! tip "Translation Tips"

  The `label` prop in the `Block` component corresponds to the translation key we defined earlier in the `common.js` file.  All text and numerical content should be translated.

If there is data, we return a Container component with three Block components, each with a label and a value.

Here we use the t function from the useTranslation hook to translate the data values. The t function takes the translation key and an object with variables to interpolate into the translation string.

We're using the common.number translation key to format the data values as numbers. This allows for easy localization of numbers, such as using commas or periods as decimal separators.

There are a large number of common numerical translation keys available, which you can learn more about in the Translation Guide.

return (
  <Container service={service}>
    <Block label="yourwidget.key1" value={t("common.number", { value: data.key1 })} />
    <Block label="yourwidget.key2" value={t("common.number", { value: data.key2 })} />
    <Block label="yourwidget.key3" value={t("common.number", { value: data.key3 })} />
  </Container>
);

Here's the complete component.jsx file:

import { useTranslation } from "next-i18next";

import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";

export default function Component({ service }) {
  const { t } = useTranslation();
  const { widget } = service;
  const { data, error } = useWidgetAPI(widget, "info");

  if (error) {
    return <Container service={service} error={error} />;
  }

  if (!data) {
    return (
      <Container service={service}>
        <Block label="yourwidget.key1" />
        <Block label="yourwidget.key2" />
        <Block label="yourwidget.key3" />
      </Container>
    );
  }

  return (
    <Container service={service}>
      <Block label="yourwidget.key1" value={t("common.number", { value: data.key1 })} />
      <Block label="yourwidget.key2" value={t("common.number", { value: data.key2 })} />
      <Block label="yourwidget.key3" value={t("common.number", { value: data.key3 })} />
    </Container>
  );
}

Add the widget to the Homepage

To add your widget to the Homepage, you need to register it in the src/widgets/widgets.js file.

Open the src/widgets/widgets.js file and import the Component from your widget's component.jsx file. Please keep the alphabetical order.

// ...
import yourwidget from "./yourwidget/widget";
// ...

Add yourwidget to the widgets object. Please keep the alphabetical order.

const widgets = {
  // ...
  yourwidget: yourwidget,
  // ...
};

You also need to add the widget to the components object in the src/widgets/components.js file.

Open the src/widgets/components.js file and import the Component from your widget's component.jsx file.

Please keep the alphabetical order.

const components = {
  // ...
  yourwidget: dynamic(() => import("./yourwidget/component")),
  // ...
};

Using the widget

You can now use your custom widget in your Homepage. Open your services.yaml file and add a new service with the yourwidget widget.

- Services:
    - Your Widget:
        icon: yourwidget.svg
        href: https://example.com/
        widget:
          type: yourwidget
          url: http://127.0.0.1:1337

!!! tip "API Tips"

You'll see here how part of the API url is built using the `url` and `endpoint` properties from the widget definition.

We defined the api endpoint as `"{url}/{endpoint}"`.  This is where the `url` is defined.  So the full API endpoint will be `http://127.0.0.1:1337/{endpoint}`.

That's it! You've successfully created a custom widget for Homepage. If you have any questions or need help, feel free to reach out to the Homepage community for assistance. Happy coding!