# Implementare interfață utilizator

## 1. Curățarea fișierului de stiluri

Mergem în tailwind.config.js și lăsăm doar liniile de configurare pentru tailwind.

```css
// tailwind.config.js

@tailwind base;
@tailwind components;
@tailwind utilities;
```

## 2. Crearea fișierului de metode pentru manipularea entry-urilor

O să ne deplasăm în folderul **utils** și o să creăm un nou fișier numit **recordsFunctions.js**.

În acest fișier, o să apelăm toate rutele create în pașii anteriori din fișierul **records.js**.

```javascript
// /utils/recordsFunctions.js

export const getRecords = async () => {
    try {
        const response = await fetch("/api/records", {
            method: "GET",
        });
    
        const data = await response.json();
    
        if (!data?.data) {
            return [];
        }
    
        return data.data;
    } catch (error) {
        console.error(error);
    }
}

export const getRecordById = async (id) => {
    try {
        const response = await fetch(`/api/records?id=${id}`, {
            method: "GET",
        });
    
        const data = await response.json();
    
        if (!data?.data) {
            return null;
        }
    
        return data.data;
    } catch (error) {
        console.error(error);
    }
}

export const createRecord = async (record) => {
    try {
        delete record._id;

        const response = await fetch("/api/records", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify(record),
        });

        const data = await response.json();

        if (!data?.data) {
            return null;
        }

        return data.data;
    } catch (error) {
        console.error(error);
    }
}

export const updateRecord = async (record) => {
    try {
        const response = await fetch("/api/records", {
            method: "PUT",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify(record),
        });

        const data = await response.json();

        if (!data?.data) {
            return null;
        }

        return data.data;
    } catch (error) {
        console.error(error);
    }
}

export const deleteRecord = async (id) => {
    try {
        const response = await fetch(`/api/records?id=${id}`, {
            method: "DELETE",
        });

        const data = await response.json();

        if (!data?.data) {
            return null;
        }

        return data.data;
    } catch (error) {
        console.error(error);
    }
}
```

## 3. Pagina princpală

O să creăm un nou folder care o să se numească components, și înăuntrul lui o să adăugăm un fișier care se va numi MainPage.jsx.

{% hint style="info" %}
Dacă folosiți **Visual Studio Code** și doriți un **shortcut** pentru un boilerplate de componentă de React, puteți tasta **rafce** și să apăsați enter și o să primiți boilerplate-ul.
{% endhint %}

```javascript
// /components/MainPage.jsx

import React from 'react'

const MainPage = () => {
  return (
    <div>MainPage</div>
  )
}

export default MainPage
```

<figure><img src="https://3115159084-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhhbPpHsnY2kIOgk08P4h%2Fuploads%2FyE7w3kOmntJETtgpnTnY%2Fimage.png?alt=media&#x26;token=0824622d-700d-4545-b74e-179caf4ea9c7" alt=""><figcaption></figcaption></figure>

Mergem în fișierul de index.js din folderul pages, ștergem tot conținutul return-ului și chemăm componenta de MainPage.jsx.

```javascript
// /pages/index.js

import MainPage from "@/components/MainPage";

export default function Home() {
  return (
    <MainPage />
  );
}
```

După efectuarea tuturor acestor modificări, începem să modificăm pagina **MainPage.jsx** și să o facem **să ceară de la baza de date toate înregistrările** **din tabela records**, și **să le mapăm** sub formă de carduri, oferind posibilitatea utilizatorilor să apese pe **butonul de editare sau de ștergere** a unui entry.

```javascript
// /components/MainPage.jsx

import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { deleteRecord, getRecords } from "@/utils/recordsFunctions";

const MainPage = () => {
  const router = useRouter();
  const [records, setRecords] = useState([]);

  const fetchRecords = async () => {
    try {
      const response = await getRecords();

      setRecords(response);
    } catch (error) {
      console.error(error);
    }
  };

  const handleDeleteRecord = async (id) => {
    try {
      const response = await deleteRecord(id);

      if (response.deletedCount === 1) {
        const newRecords = records.filter((record) => record._id !== id);
        setRecords(newRecords);
      }
    } catch (error) {
      console.log(error)
    }
  };

  const handleUpdateRecord = (id) => {
    router.push(`/records/edit?id=${id}`);
  };

  useEffect(() => {
    fetchRecords();
  }, []);

  return (
    <div className="p-4 flex flex-wrap gap-4">
      {records.map((record) => (
        <div
          className="max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700"
          key={record._id}
        >
          <h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
            {record.title}
          </h5>
          <p className="mb-3 font-normal text-gray-700 dark:text-gray-400">
            {record.description}
          </p>
          <div className="flex justify-center">
            <button
              type="button"
              className="text-white bg-gradient-to-r from-green-400 via-green-500 to-green-600 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-green-300 dark:focus:ring-green-800 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2"
              onClick={() => handleUpdateRecord(record._id)}
            >
              Update
            </button>
            <button
              type="button"
              className="text-white bg-gradient-to-r from-red-400 via-red-500 to-red-600 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2"
              onClick={() => handleDeleteRecord(record._id)}
            >
              Delete
            </button>
          </div>
        </div>
      ))}
    </div>
  );
};

export default MainPage;
```

{% hint style="info" %}
Această componentă folosește două hook-uri foarte importante puse la dispoziție de către React:

1. **useState** -> care ne ajută să menținem valori pe care să le urmărim și să ne folosim de ele într-un mod dinamic atunci când aceste sunt actualizate.

În cazul nostru, salvăm lista de records pe care o mapăm sub formă de carduri.&#x20;

<mark style="color:red;">**!!**</mark> **Metoda de set este async** <mark style="color:red;">**!!**</mark>

2. **useEffect** -> un hook care are rolul de a efectua anumite acțiuni atunci când o variabilă din array-ul de dependențe se schimbă, sau de a efectua o singură acțiune atunci când componenta se montează și array-ul de dependințe este gol.

În cazul nostru, ne-am folosit de useEffect pentr a apela metoda **fetchRecords** care cere la baza de date lista cu toate entry-urile.
{% endhint %}

{% hint style="info" %}
Cel mai important lucru pe care trebuie să îl facem atunci când dorim să mapăm o listă de valori, este să oferim fiecărui item mapat **o cheie unică** de identificare care să ajute sistemul să identifice în orice secundă ce element se dorește a fi modificat/șters.
{% endhint %}

## 4. Crearea unui entry

Pentru crearea unui nou entry, o să ne mutăm în folderul **pages** și o să creăm un nou folder numit **records** cu un fișier **create.jsx**.

```javascript
// /pages/records/create.jsx

import RecordForm from "@/components/RecordForm";
import { recordDefaultValues } from "@/utils/constants";
import { createRecord } from "@/utils/recordsFunctions";
import { useRouter } from "next/router";

const Create = () => {
  const router = useRouter();
  const entry = recordDefaultValues;

  const onSubmit = async (data) => {
    const response = await createRecord(data);

    if (response) {
      router.push("/");
    } else {
      alert("Failed to create record");
    }
  }

  return (
      <RecordForm data={entry} onSubmit={onSubmit} />
  );
};

export default Create;
```

{% hint style="info" %}
Ruta poate fi accesată la [http://localhost:3000/record](http://localhost:3000/records/edit?id=660b10784bc8b8175525cba3)[s/create](http://localhost:3000/records/create)
{% endhint %}

După cum putem vedea, am creat **un entry** pe baza unui template și o **metodă de submit (callback function)**, pe care le-am pasat către altă componentă sub formă de **PROPS**.

{% hint style="info" %}
Un **callback function** este o funcție pasată ca și prop unei componente copil, cu scopul ca acesta să o poată apela oricând are nevoie.

Pentru mai multe detalii despre callback functions: <https://developer.mozilla.org/en-US/docs/Glossary/Callback_function>
{% endhint %}

Pentru a crea respectivul template de entry, o să ne mutăm în folderul utils, și o să creăm fișierul **constants.js**.

```javascript
// /utils/constants.js

export const recordDefaultValues = {
    _id: "",
    title: "",
    description: ""
};
```

Pentru crearea formularului, o să ne mutăm în folderul **components** și o să creăm fișierul **RecordForm.jsx**.

```javascript
// /components/RecordForm.jsx

import React, { useState } from "react";
import { useRouter } from "next/router";

const RecordForm = (props) => {
  const { data, onSubmit } = props;
  const router = useRouter();
  const [entry, setEntry] = useState(data);

  const updateEntry = (type, value) => {
    setEntry({ ...entry, [type]: value });
  };

  const handleCancel = () => {
    router.push("/");
  }

  return (
    <div className="flex justify-center p-4">
      <div className="border p-4 rounded-md shadow-sm flex flex-col gap-4 w-full max-w-80">
        <div>
          <label
            htmlFor="title"
            className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
          >
            Title
          </label>
          <input
            type="text"
            id="title"
            value={entry.title}
            onChange={(e) => updateEntry("title", e.target.value)}
            className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
            placeholder="Title"
            required
          />
        </div>
        <div>
          <label
            htmlFor="description"
            className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
          >
            Description
          </label>
          <textarea
            id="description"
            rows="4"
            value={entry.description}
            onChange={(e) => updateEntry("description", e.target.value)}
            className="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
            placeholder="Write your thoughts here..."
          ></textarea>
        </div>
        <div className="w-full flex justify-center gap-4">
            <button
            type="button"
            onClick={handleCancel}
            className="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900"
          >
            Cancel
          </button>
          <button
            type="button"
            onClick={() => onSubmit(entry)}
            className="focus:outline-none text-white bg-green-700 hover:bg-green-800 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800"
          >
            {entry._id ? "Update" : "Create"}
          </button>
        </div>
      </div>
    </div>
  );
};

export default RecordForm;
```

{% hint style="info" %}
După cum putem observa, componenta **RecordForm** primește ca și props **data** și **onSubmit**, **setează un state cu obiectul data primit**, și **umple valorile input-urilor cu valorile primite**, punând la dispoziție metoda onSubmit atunci când se apasă pe butonul **Create/Update**.
{% endhint %}

## 5. Editarea unui entry

Ne mutăm în folderul **records** din **pages** și adăugăm o nouă componentă, numită **edit.jsx**.

```javascript
// /pages/records/edit.jsx

import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import Spinner from "@/components/Spinner";
import { recordDefaultValues } from "@/utils/constants";
import { getRecordById, updateRecord } from "@/utils/recordsFunctions";
import RecordForm from "@/components/RecordForm";

const Edit = () => {
  const router = useRouter();
  const [isLoading, setIsLoading] = useState(true);
  const [entry, setEntry] = useState(recordDefaultValues);

  const getRecord = async (id) => {
    const data = await getRecordById(id);

    if (data) {
      setEntry(data);
    }

    setIsLoading(false);
  };

  const onSubmit = async (data) => {
    const response = await updateRecord(data);

    if (response) {
      router.push("/");
    } else {
      alert("Failed to update record");
    }
  }

  useEffect(() => {
    const searchParams = new URLSearchParams(router.asPath.split(/\?/)[1]);
    const id = searchParams.get("id");

    if (!id) {
      router.push("/");
    }

    getRecord(id);
  }, []);

  if (isLoading) {
    return <Spinner />;
  }

  return (
    <>
      {entry._id ? (
        <RecordForm data={entry} onSubmit={onSubmit}/>
      ) : (
        <div className="text-center">Record not found</div>
      )}
    </>
  );
};

export default Edit;
```

{% hint style="info" %}
Principalul rol al acestei componente este ca atunci când este montată (prin intermediul **useEffect-ului**) să verifice dacă în path-ul accesat există sau nu un **Query Param** cu id-ul record-ului care se dorește a fi editat.&#x20;

* În cazul în care găsește id-ul, va încerca să tragă de la baza de date record-ul cu id-ul furnizat. În cazul fericit, o să paseze către componenta RecordForm următoarele prop-uri:
  * **entry** -> entry-ul găsit în baza de date
  * **onSubmit** -> un callback function care are rolul de a actualiza entry-ul atunci când componenta copil cere acest lucru.
* În cazul în care **nu se găsește id-ul ca și Query Param**, utilizatorul va fi redirecționat către **HomePage**.

A fost implementată și funcționalitatea de **loading**, astfel încât până când nu se primește de la server un răspuns pozitiv sau nu, un **Spinner** va fi afișat pe ecran.
{% endhint %}

{% hint style="info" %}
Pentru testare;

* <http://localhost:3000/records/edit?id=1> -> va încerca să găsească în baza de date entry-ul cu id-ul 1.
* [http://localhost:3000/records/edit](http://localhost:3000/records/edit?id=1) -> va redirecționa utilizatorul către **HomePage**, deoarece nu este furnizat un entry id.
  {% endhint %}

Pentru crearea Spinner-ului, o să mergem în folderul **components** și o să creăm un fișier numit **Spinner.jsx**.

```javascript
// /components/Spinner.jsx

import React from "react";

const Spinner = () => {
  return (
    <div className="flex w-full h-screen justify-center items-center">
      <svg
        aria-hidden="true"
        className="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600"
        viewBox="0 0 100 101"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path
          d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
          fill="currentColor"
        />
        <path
          d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
          fill="currentFill"
        />
      </svg>
      <span className="sr-only">Loading...</span>
    </div>
  );
};

export default Spinner;
```
