An FPL asset management board built using React, Xata & Cloudinary

Damilare - Nov 23 '22 - - Dev Community

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


Creating the Xata Database

To get started with Xata, create an account here and follow the steps provided in the docs.

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

Xata workspace

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

Database schema

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

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:

#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

Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

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:

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.
Enter fullscreen mode Exit fullscreen mode

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

    // 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;
    };
Enter fullscreen mode Exit fullscreen mode

In the defaultOptions object set the enableBrowser option to true:

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

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

    #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;
    }
Enter fullscreen mode Exit fullscreen mode

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

    <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"
        />
Enter fullscreen mode Exit fullscreen mode

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

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;

Enter fullscreen mode Exit fullscreen mode

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

    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;
Enter fullscreen mode Exit fullscreen mode

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

    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>
      );
    }
Enter fullscreen mode Exit fullscreen mode

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:

    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;
Enter fullscreen mode Exit fullscreen mode

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, registering, logging in, accessing your media library, and uploading the logo, which you can find here.

Cloudinary media library interface

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

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));
...

Enter fullscreen mode Exit fullscreen mode

Replace 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.

    return(
    ...
      <header className="header">
            <span>
              <AdvancedImage cldImg={Image} />
            </span>
            <span>
              <h2>Assets</h2>
            </span>
          </header>
     ...)
Enter fullscreen mode Exit fullscreen mode

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; my application instance is here. 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.

Sources

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .