Creating a custom widget for Notion
Tags: react, nextjs, tailwindcss
Published: 2021-09-12 22:11:12

Hello, I'm back.

Today we are going to build a widget for Notion, using the `dev.to` API, to show the latest articles from our favorite authors.

✨ You can see the live demo at: 
https://notion-widget-dev-to.vercel.app/?users=devrchancay,alexandprivate,dabit3


#### Disclaimer:
_this project uses next, tailwind, typescript, NPM to generate a simple widget (This is overkill for this demo, I know) 😬_

_You know that you can achieve the same result with HTML + CSS + JavaScript. Maybe in the future I will add more widgets to justify the use of all those technologies._


To do it, we are going to use `NextJS` and `TailwindCSS`.

## Start project [Nextjs]

To start the project, we execute the following command:

```bash
$ npx create-next-app dev-to-widget --use-npm -e with-typescript
```

With that we already have the `dev-to-widget` directory, with a nextjs project, which uses `npm` and `Typescript`.


## Add Tailwind to the project

We install the following dependencies: 

```bash
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
```

Then we generate the configuration files:

```bash
$ npx tailwindcss init -p
```

Now, we have the files `tailwind.config.js` and `postcss.config.js` in the root of the project.


Now, we modify "purge" in the tailwind settings, to include the `page` and` components` directory, for when the build is generated to remove CSS that we are not using.

```js
// tailwind.config.js
  module.exports = {
   purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
    darkMode: false,
    theme: {
      extend: {},
    },
    variants: {
      extend: {},
    },
    plugins: [],
  }
```
And finally, we add tailwind in our `pages/_app.tsx` file.

```tsx
import { AppProps } from "next/app";
import "tailwindcss/tailwind.css";

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

export default MyApp;
```


## Next SSR

The widget works from a parameter in the URL called `users` that contains the usernames separated by a `,`

For example:
 ?users=devrchancay,alexandprivate

> With this parameter the widget will be rendered with "devrchancay" and "alexandprivate" in that order.

```tsx
export const getServerSideProps = async ({ query }) => {
  const users = query?.users?.split(",") ?? [];

  const usersPromise = users.map((user) =>
    fetch(`https://dev.to/api/articles?username=${user}`).then((user) =>
      user.json()
    )
  );

  const blogPosts = await Promise.all(usersPromise);

  return {
    props: {
      blogPosts,
    },
  };
};
```

### Let me explain:

- I convert the string separated by ',' into an array.
```js
 const users = query?.users?.split(",") ?? [];
// ['devrchancay', 'alexandprivate']
```
- Generated an array with the requests to the API with each user.

```js
const usersPromise = users.map((user) =>
    fetch(`https://dev.to/api/articles?username=${user}`).then((user) =>
      user.json()
    )
  );

// [Promise<pending>(devrchancay), Promise<pending>(alexandprivate)]
```

- I resolve the promises and save them in an array that contains the articles of each author in the order that they were entered in the URL.

```js
 const blogPosts = await Promise.all(usersPromise);
// [devrchancay-articles, alexandprivate-articles]
```

- I send the component to render the widget.

```js
return {
    props: {
      blogPosts,
    },
  };
```

- And finally, we render the component.

```tsx
const IndexPage = ({ blogPosts }) => {
  const router = useRouter();
  const usersQuery = router?.query?.users as string;
  const users = usersQuery?.split(",") ?? [];

  const [currentIndex, setCurrentIndex] = useState(0);

  const usersString = users.join(", ");

  return (
    <div>
      <Head>
        <title>Posts: {usersString}</title>
        <meta name="description" content={`dev.to posts ${usersString}}`} />
      </Head>

      <div className="max-w-xl mx-auto sm:overflow-x-hidden">
        {blogPosts[currentIndex]?.map((post) => (
          <div key={post.id} className="mb-4">
            {post.cover_image && (
              <div className="relative max-w-xl h-52">
                <Image src={post.cover_image} alt={post.title} layout="fill" />
              </div>
            )}
            <div className="py-2 px-2">
              <div>
                {post.tag_list.map((tag) => (
                  <a
                    key={tag}
                    target="_blank"
                    rel="noopener"
                    href={`https://dev.to/t/${tag}`}
                    className="mr-2"
                  >
                    #<span className="text-gray-900">{tag}</span>
                  </a>
                ))}
              </div>
              <h1 className="text-3xl tracking-tight font-extrabold text-gray-900 sm:text-4xl">
                {post.title}
              </h1>

              <p className="mt-3 text-xl text-gray-500 sm:mt-4">
                {post.description}
              </p>
              <a
                target="_blank"
                rel="noopener"
                className="text-base font-semibold text-indigo-600 hover:text-indigo-500"
                href={post.url}
              >
                Read full story
              </a>
            </div>
          </div>
        ))}
        <ul className="w-full overflow-x-scroll flex space-x-6 px-2 sticky bottom-0 bg-white z-50">
          {users.map((user, index) => (
            <li
              key={user}
              className={`py-2 ${
                currentIndex === index
                  ? "border-t-4 border-indigo-600"
                  : "border-t-4 border-transparent"
              } `}
            >
              <a
                href="/"
                className="text-center"
                onClick={(evt) => {
                  evt.preventDefault();
                  setCurrentIndex(index);
                }}
              >
                {user}
              </a>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
};
```


## The widget working!

I use this widget in my personal workspace.

> They should add it as an embed and resize it to their liking.

![Notion widget](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dh05ox4zpo7negycfwsa.jpeg)


you can see the complete code in the repository: https://github.com/devrchancay/notion-widget-dev-to/blob/main/pages/index.tsx