使用React,XATA和Cloudinary构建的FPL资产管理委员会
#网络开发人员 #react #database #xata

使用数据集时,重要的第一步是找到一个视图,使您可以一目了然地查看所需的所有相关信息,无论是通过列出项目,将它们分组为表中的列,还是正如我将在本文中证明的那样,您可以根据其状态无缝移动项目。

现在将XATA与React集成以创建具有以下功能的板:

  1. 持久数据
  2. 创建,阅读,更新和删除资产
  3. 设置资产的可用性,即,如果玩家受伤或暂停
  4. 单击并删除功能以管理球员的状态与您的团队有关

我们将使用以下工具:

  • Xata
  • React
  • Vite

XATA是一项数据库服务,可提供内置分析,搜索引擎和易于使用的电子表格式用户界面,以及其他出色的功能。

React是一个JavaScript库,用于构建用户界面,并具有围绕其构建的重要工具生态系统。

vite是一种构建工具,旨在为当前的Web项目提供更快,更精美的开发体验。

Cloudinary提供基于云的图像和视频管理服务,包括存储和交付,我们现在将利用这些服务将幻想徽标添加到我们的应用中。

对于那些想知道的人,FPL(幻想英超联赛)是一款幻想足球比赛,全球拥有超过1000万球员。

这是一个完整的应用程序:

The finished project


创建XATA数据库

要开始使用XATA,请创建一个帐户here并按照docs中提供的步骤。

登录工作空间,您应该看到以下屏幕。

Xata workspace

创建数据库并使用以下模式创建表:

Database schema

资产表是用于构建此应用程序的唯一使用的表,我将仔细研究我在数据库上设置的详细信息和约束。有四列可以存储每个记录的关联值,所有列类型都是字符串。

id字段默认情况下包含一个自动生成的值。 name字段设置为唯一,因此没有两个资产可以具有相同的值,因为它在开发UI时用于过滤值。需要status字段,因此如果尝试在不设置状态的情况下创建记录,则会丢弃错误。

Asset creation form

上图显示了创建新记录所需的形式,而不是ORM或RAWSQLðä。

现在已经处理了所有数据库要求,我们将继续构建前端接口并使用SDK将其连接到XATA。


构建React前端

我为前端选择了反应[最受欢迎的UI库]就不足为奇了。另一方面,我将vite作为我的构建工具。它在VUE社区中更广泛地使用,但是它提供了来自其他前端框架的模板,并且效果非常好。让我们看看它的行动:

#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

就是这样。您可以自由地将其与您选择的其他任何脚手架框架交换
要在应用程序中添加XATA,请从安装全球CLI

开始

npm i -g @xata.io/cli

按照指示here创建API键并初始化您的项目。

初始化后,将xata init生成的src目录中的文件移动到您的React Apps src文件夹中。

现在,将XATA客户端发生器导入您的App.jsx,调用并导出它,以便其他组件可以访问它。

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

完成此操作后,我们准备构建应用程序的前端。尽管现在是提及XATA更适合具有服务器端功能的框架的好时机,例如Next,Nuxt和Svelte套件。

与浏览器一起使用XATA时,有可能暴露API密钥,因此,如果您尝试在浏览器中运行它,您将在控制台中看到此消息:

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.

通过修改defaultOptions对象在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;
    };

将默认选项对象更改为:

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

现在添加CSS,打开App.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;
    }

并在index.html中包含以下链接以在此项目中使用字体真棒图标:

    <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"
        />

现在我们准备做出反应! ð

组件
本节将显示用于构建应用程序及其功能的组件。
使用以下成员创建一个components目录并插入以下代码:

  • 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;

此组件显示一张带有有关单个资产(即名称和可用性)的详细信息的卡。您可以在3个不同的选项之间选择,并根据首选选项进行更改。

它还包含带有stopPropagation集的onClick事件中的删除功能,因此不会干扰点击功能。调用数据库以删除和更新资产的可用性。

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;

此组件包含将新资产添加到列表中的表单。我有一个onFormClick道具,可以在提交时切换其可见性。提交后,调用数据库,创建新资产,并更新本地状态以显示更改。

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

此组件列出了已创建的特定状态的所有资产。它仅设置为允许用户切换表单以创建新资产,而其中包含的资产数量小于11。


最后,将所有内容放在App.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;

在App root中,我们使用useEffect获取数据库中的初始数据,通过其当前状态过滤卡并根据其当前状态将其提供给AssetList组件。

最后,这包含处理单击功能的moveCard功能,以在不同列表之间移动资产。


带有云的图像存储

Cloudinary提供基于云的图像和视频管理服务,包括存储和交付,我们现在将利用这些服务将幻想徽标添加到我们的应用中。

首先创建一个帐户,registering,登录,访问媒体库和上传徽标,您可以找到here

Cloudinary media library interface

要使它与React一起使用,您必须安装Cloudinary的React库:

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

现在将以下代码添加到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));
...

用cloud_name替换,在您创建帐户时会自动分配。访问Cloudinary的动态URL基于您的Cloud_name。

之后,在应用程序标题中包括新创建的图像,然后完成。

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

这仅刮擦云的表面。从快速内容交付网络到基于面部检测的自动转换,同时为每种广泛使用的编程语言提供易于使用的API。对于需要任何形式的图像或视频处理的各种项目的项目,这是一项方便的服务。

结论

您可以找到所有的code;我的应用程序是here。例如,您可以通过像官方的Xata tutorial中的服务器端框架中复制本文中提出的想法。

来源