An FPL asset management board built using React, Xata & Cloudinary
Tags: react, webdev, database, xata
Published: 2022-11-23 21:48:20

When working with data sets, an essential first step is finding a view that lets you see all the relevant information you need from it at a glance, whether it's by listing out items, grouping them into columns in a table, or, as I'll demonstrate in this article a board that allows you to move items seamlessly based on their status.

Now to integrate Xata with React to create a board with the following features:

1. Persisted data
2. Create, Read, Update and Delete Assets
3. Set the availability of the assets, i.e., if the player is injured or suspended
4. Click and drop functionality to manage the player's status in relation your team

We'll use the following tools:

- Xata
- React
- Vite 
- Cloudinary

Xata is a database service that provides built-in analytics, a search engine, and an easy-to-use spreadsheet-like user interface, among other great features.

React is a JavaScript library for building user interfaces with a great ecosystem of tools built around it.

Vite is a build tool that aims to provide faster and leaner development experience for current web projects.

Cloudinary provides cloud-based image and video management services, including storage and delivery, which we will take advantage of now to add the fantasy logo to our App.

And for those wondering, FPL (Fantasy Premier League) is a fantasy football game with over 10 million players worldwide. 

Here's a look at the complete application:

![The finished project](https://paper-attachments.dropboxusercontent.com/s_A0032CFE51EA0DFEA1E2A9C1A2620E6C996D1FCD14EFC39C2BB61E2615105879_1667653384724_image.png)

----------
## Creating the Xata Database

To get started with Xata, create an account [here](https://app.xata.io/signin?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) and follow the steps provided in the [docs](https://xata.io/docs/intro/getting-started?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog).

Logged into your workspace, you should see the following screen.

![Xata workspace](https://paper-attachments.dropboxusercontent.com/s_A0032CFE51EA0DFEA1E2A9C1A2620E6C996D1FCD14EFC39C2BB61E2615105879_1667650456266_image.png)


Create your database and create your table with the following schema:

![Database schema](https://paper-attachments.dropboxusercontent.com/s_A0032CFE51EA0DFEA1E2A9C1A2620E6C996D1FCD14EFC39C2BB61E2615105879_1667650249533_image.png)


The assets table is the only one used in building this App, and I will go through the details and constraints I set on the database. There are four columns to store the associated values of each record, and all the column types are strings.

The `id` field contains an auto-generated value by default. The `name` field is set to be unique, so no two assets can have the same value for this field since it is used in filtering values later when developing the UI. The `status` field is required, so an error is thrown if an attempt is made to create a record without setting a status.

![Asset creation form](https://paper-attachments.dropboxusercontent.com/s_A0032CFE51EA0DFEA1E2A9C1A2620E6C996D1FCD14EFC39C2BB61E2615105879_1667651315631_image.png)


The image above shows the form required to create a new record instead of an ORM or raw SQL 🤫 .

Now that all the database requirements have been handled, we'll move on to building the front-end interface and connecting it to Xata using the SDK.

----------
## Building the React front end

No surprise that I've chosen React [the most popular UI library] for the front end. On the other hand, I've gone with `vite` as my build tool. It's more widely used in the Vue community, but it provides templates from other front-end frameworks and works amazingly well. Let's see it in action:

```shell
#scaffold the project you can replace `fpl` with a preferred project name
yarn create vite fpl --template react
#change directories to the newly created project
cd fpl
#run yarn to install packages
yarn

```

And that's it. You're free to swap it out with any other scaffolding framework of your choice
To add Xata to the application, start by installing the CLI globally


`npm i -g @xata.io/cli`

Follow the instructions [here](https://xata.io/docs/quickstart/building-a-web-app-with-xata-and-nextjs#adding-xata-to-our-application?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) to create an API key and initialize your project. 

After initialization, move the files in the `src` directory generated by `xata init` to your react apps `src` folder. 

Now import the xata client generator into your `App``.``jsx`, call it, and export it so other components can access it. 


```jsx
import { useEffect, useState } from "react";
import { getXataClient } from "./xata";
import "./App.css";
export const xata = getXataClient();

```

Once that's done, we're ready to build the front end of our application. Although now is a good time to mention that Xata is better suited for frameworks with server-side capabilities, e.g., Next, Nuxt, and Svelte Kit.

There's the risk of exposing your API key when you work with Xata from a browser, so you'll see this message in your console if you try to run it within the browser:


```shell!
Uncaught Error: You are trying to use Xata from the browser, which is potentially a non-secure environment. If you understand the security concerns, such as leaking your credentials, pass `enableBrowser: true` to the client options to remove this error.
```

This is easily fixed by modifying the `defaultOptions` object in `xata.js`:

```javascript
    // Generated by Xata Codegen 0.18.0. Please do not edit.
    import { buildClient } from "@xata.io/client";
    /** @typedef { import('./types').SchemaTables } SchemaTables */
    /** @type { SchemaTables } */
    const tables = [
      {
        name: "Assets",
        columns: [
          { name: "name", type: "string", unique: true },
          { name: "status", type: "string", notNull: true, defaultValue: "in" },
          { name: "availability", type: "string" },
        ],
      },
    ];
    /** @type { import('@xata.io/client').ClientConstructor<{}> } */
    const DatabaseClient = buildClient();
    const defaultOptions = {
      databaseURL:
        "https://DAMILARE-s-workspace-uaav04.us-east-1.xata.sh/db/hackathon",
    };
    /** @typedef { import('./types').DatabaseSchema } DatabaseSchema */
    /** @extends DatabaseClient<DatabaseSchema> */
    export class XataClient extends DatabaseClient {
      constructor(options) {
        super({ ...defaultOptions, ...options }, tables);
      }
    }
    let instance = undefined;
    /** @type { () => XataClient } */
    export const getXataClient = () => {
      if (instance) return instance;
      instance = new XataClient();
      return instance;
    };
```
    

In the `defaultOptions` object set the `enableBrowser` option to `true`:

```javascript
    const defaultOptions = {
      databaseURL:
        "https://DAMILARE-s-workspace-uaav04.us-east-1.xata.sh/db/hackathon",
      enableBrowser: true,
    };
```

Let's now add the CSS, open the `App.css` file and add the following rulesets:

```css
    #root {
      max-width: 1280px;
      margin: 0 auto;
      padding: 2rem;
      text-align: center;
    }
    
    .header {
      display: flex;
      gap: 1em;
      justify-content: center;
      align-items: center;
    }
    .index {
      display: grid;
      height: 100vh;
      grid-template-rows: auto 1fr auto;
      row-gap: .4em;
    }
    .board {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      column-gap: .2em;
    }
    .assets {
      display: grid;
      grid-template-rows: repeat(11, 1fr);
      background-color: rgb(250, 189, 189);
    }
    .asset-card {
      background-color: rgba(255, 255, 255, 0.925);
      border: solid 1px grey;
      padding: .8em;
      margin-inline: .4em;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .avail-selector {
      appearance: none;
      background-color: #02bafe;
      color: white;
      border-radius: 3.2em;
      height: 2em;
      margin-left: 0;
      width: 8em;
      text-align: center;
      border: none;
    }
    .suspended {
      background-color: rgb(156, 86, 226);
    }
    .injured {
      background-color: rgb(158, 158, 8);
    }
    .fa-trash {
      color: rgb(250, 66, 66);
    }
    .hide {
      display: none;
    }
    .add-asset-form {
      display: flex;
      flex-direction: column;
      gap: 2px;
    }
```

And include the following link in `index.html` to use the font awesome icons in this project:

```htmlembedded
    <link
          rel="stylesheet"
          href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
          integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
          crossorigin="anonymous"
          referrerpolicy="no-referrer"
        />
```


----------

Now we're ready to React! 😁 

**Components**
This section will show the components used in building the application and their functions.
Create a `components` directory with the following members and insert the following code:

- AssetList.jsx
- AddAssetForm.jsx
- AssetDetail.jsx

`AssetDetail.jsx`

```jsx
import React, { useState } from "react";
import { xata } from "../App";

function AssetDetail({ asset, deleteAsset, onClick }) {
  const optionsAvailable = ["fit", "injured", "suspended"];
  const [availability, setAvailability] = useState(asset.availability);
  const { id } = asset;
  const handleChange = (e) => {
    let availability = e.target.value;
    xata.db.Assets.update({ availability, id });
    setAvailability(availability);
  };
  return (
    <p className="asset-card" key={asset.id} onClick={() => onClick(asset)}>
      <span>{asset.name}</span>{" "}
      <select
        name="availability"
        onChange={handleChange}
        className={`avail-selector ${availability}`}
      >
        <option value={asset.availability}>{asset.availability}</option>
        {optionsAvailable.map(
          (option) =>
            option != asset.availability && (
              <option value={option} key={option}>
                {option}
              </option>
            )
        )}
      </select>
      <i
        className="fa-solid fa-trash"
        onClick={(e) => {
          deleteAsset(asset.name, asset.id);
          e.stopPropagation();
        }}
      />
    </p>
  );
}
export default AssetDetail;

```

This component displays a card with the details about a single asset, i.e., name and availability. You can select between 3 different options, and the styling changes depending on the preferred option. 

It also contains a delete functionality in an `onClick` event with a `stopPropagation` set, so it doesn't interfere with the click-and-drop functionality. Calls are made to the database to delete and update an asset's availability.

`AddAssetForm.jsx`
```jsx
    import React from "react";
    import { useState } from "react";
    import { xata } from "../App";
    function AddAssetForm({ status, show, onFormClick, updateAssets }) {
      const [name, setName] = useState("");
      //   const [status, setStatus] = useState(status);
      const [availability, setAvail] = useState("");
      const handleChange = (e) => setName(e.target.value);
      const handleSubmit = (e) => {
        e.preventDefault();
        updateAssets({ name, availability, status });
        xata.db.Assets.create({ name, availability, status });
        setName("");
        setAvail("");
        onFormClick();
      };
      return (
        <div>
          <form
            action="post"
            onSubmit={handleSubmit}
            className={!show ? "hide" : "add-asset-form"}
          >
            Add Asset
            <input type="text" value={name} onChange={handleChange} />
            <input
              type="text"
              name="availability"
              value={availability}
              onChange={(e) => setAvail(e.target.value)}
            />
            <input type="submit" value="add player" />
          </form>
        </div>
      );
    }
    export default AddAssetForm;
```
    

This component contains the form to add new assets to the list. I have an `onFormClick` prop that toggles its visibility when submitted. Upon submission, the database is called, the new asset is created, and the local state is updated to display the changes.

`AssetList.jsx`
```jsx
    import { useState } from "react";
    import { xata } from "../App";
    import AddAssetForm from "./AddAssetForm";
    import AssetDetail from "./AssetDetail";
    export default function AssetList({
      assets,
      status,
      updateAsset,
      allAssets,
      selectAssetCardOnClick,
      moveCard,
    }) {
      const [showForm, setShowForm] = useState(false);
      const updateAssetsDisplayed = (newAsset) => {
        updateAsset(allAssets.concat(newAsset));
      };
      const handleClick = () => {
        setShowForm(!showForm);
      };
      const deleteAsset = (assetName, assetID) => {
        xata.db.Assets.delete(assetID);
        updateAsset(allAssets.filter((asset) => asset.name !== assetName));
      };
      return (
        <div className="assets" onClick={() => moveCard(status)}>
          {assets.map((asset) => (
            <AssetDetail
              asset={asset}
              allAssets={allAssets}
              deleteAsset={deleteAsset}
              onClick={selectAssetCardOnClick}
              key={asset.id}
            />
          ))}
          {assets.length < 11 ? (
            <>
              <AddAssetForm
                status={status}
                show={showForm}
                onFormClick={handleClick}
                updateAssets={updateAssetsDisplayed}
              />
              <i className="fa-regular fa-plus" onClick={handleClick} />
            </>
          ) : (
            ""
          )}
        </div>
      );
    }
```
    

This component lists all the assets of a particular status that have been created. It's set only to allow the user to toggle the form to create new assets while the number of assets contained in it is less than 11.

----------

Finally, bring it all together in the `App.jsx` file:

```jsx
    import { useEffect, useState } from "react";
    import { getXataClient } from "./xata";
    import "./App.css";
    import AssetList from "./components/AssetList";
    export const xata = getXataClient();
    function App() {
      const [assets, setAssets] = useState([]);
      const [selectedCard, setSelectedCard] = useState(null);
      useEffect(() => {
        (async () => {
          const data = await xata.db.Assets.getAll();
          setAssets(data);
        })();
      }, []);
      const handleCardSelect = (currentCardName) => {
        setSelectedCard(currentCardName);
      };
      const moveCard = (newStatus) => {
        try {
          const { id } = selectedCard;
          if (selectedCard) {
            xata.db.Assets.update({ id, status: newStatus }),
              setAssets((prevState) => [
                ...prevState.filter((asset) => asset.name !== selectedCard.name),
                { ...selectedCard, status: newStatus },
              ]);
            setSelectedCard(null);
          }
        } catch (e) {}
      };
      const assetsIn = assets.filter(
        (asset) => asset.status.toLowerCase() === "in"
      );
      const assetsOut = assets.filter(
        (asset) => asset.status.toLowerCase() === "out"
      );
      const assetsWatch = assets.filter(
        (asset) => asset.status.toLowerCase() === "watch"
      );
      return (
        <div className="index">
          <header className="header">FPL Assets</header>
          <div className="board">
            <span>
              <h3>In</h3>
              <AssetList
                assets={assetsIn}
                allAssets={assets}
                status="In"
                updateAsset={setAssets}
                selectAssetCardOnClick={handleCardSelect}
                moveCard={moveCard}
              />
            </span>
            <span>
              <h3>Watching</h3>
              <AssetList
                assets={assetsWatch}
                allAssets={assets}
                status="Watch"
                selectAssetCardOnClick={handleCardSelect}
                updateAsset={setAssets}
                moveCard={moveCard}
              />
            </span>
            <span>
              <h3>Out</h3>
              <AssetList
                assets={assetsOut}
                allAssets={assets}
                status="Out"
                updateAsset={setAssets}
                selectAssetCardOnClick={handleCardSelect}
                moveCard={moveCard}
              />
            </span>
          </div>
          <footer>and some bottom stuff</footer>
        </div>
      );
    }
    export default App;
```
    

In the App root, we fetch the initial data from the database with `useEffect`, filter the cards by their current status and provide them to the `AssetList` components as appropriate. 

Finally, this contains the `moveCard` function that handles the click-and-drop functionality to move assets between different lists.

----------
## Image storage with Cloudinary

Cloudinary provides cloud-based image and video management services, including storage and delivery, which we will take advantage of now to add the fantasy logo to our App.

Start by creating an account, [register](https://cloudinary.com/users/register_free?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog)[ing](https://cloudinary.com/users/register_free), logging in, accessing your media library, and uploading the logo, which you can find [here](https://res.cloudinary.com/dsuqfsnp2/image/upload/v1667707134/FPL-2223-EDITORIAL-STATEMENT_2_kd8sgw.png?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog).

![Cloudinary media library interface](https://paper-attachments.dropboxusercontent.com/s_A0032CFE51EA0DFEA1E2A9C1A2620E6C996D1FCD14EFC39C2BB61E2615105879_1667712959010_image.png)


To get it to work with React, you have to install Cloudinary's React library:

`yarn add @cloudinary/url-gen @cloudinary/react`


Now add the following code to `App.jsx`

```jsx
import { Cloudinary } from "@cloudinary/url-gen";
import { AdvancedImage } from "@cloudinary/react";
import { fill } from "@cloudinary/url-gen/actions/resize";
function App() {
  ...
  const cld = new Cloudinary({
    cloud: {
      cloudName: <CLOUD_NAME>,
    },
  });
  const Image = cld
    .image("FPL-2223-EDITORIAL-STATEMENT_2_kd8sgw.png")
    .resize(fill().width(200).height(50));
...

```

Replace <CLOUD_NAME> with your cloud_name, which was automatically assigned when you created your account. The dynamic URLs for accessing Cloudinary are based on your cloud_name.

After that, include the newly created image in the app header, and it is done.
```jsx
    return(
    ...
      <header className="header">
            <span>
              <AdvancedImage cldImg={Image} />
            </span>
            <span>
              <h2>Assets</h2>
            </span>
          </header>
     ...)
```

This only scratches the surface of what can be done with Cloudinary. From the fast content delivery network to automatic face detection-based transformations while providing easy-to-use APIs for every widely used programming language. It is a convenient service for projects of all sizes requiring any form of image or video handling.

## Conclusion

You can find all the [code](https://github.com/themilar/FPL-assets-manager); my application instance is [here](https://celebrated-heliotrope-d448d9.netlify.app/). For example, you can extend the ideas presented in this article by replicating them in a server-side framework like in the official Xata [tutorial](https://xata.io/docs/quickstart/building-a-web-app-with-xata-and-nextjs?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog).

## Sources
* [When to use each type of database view ](https://www.notion.so/help/guides/when-to-use-each-type-of-database-view)
* [Xata docs](https://xata.io/docs/intro/getting-started)
* [Cloudinary docs](https://cloudinary.com/documentation)