> For the complete documentation index, see [llms.txt](https://lungu-mihai-adrian.gitbook.io/cloud-computing-2026-simpre/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lungu-mihai-adrian.gitbook.io/cloud-computing-2026-simpre/seminar-2/implementare-interfata-utilizator.md).

# Implementare interfață utilizator

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

Mergem în tailwind.config.js și lăsăm doar linia de import pentru Tailwind.

```css
// globals.css

@import "tailwindcss";
```

## 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 în backend din fișierul **records.js**.

{% code expandable="true" %}

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

export const getRecords = async () => {
  const response = await fetch('/api/records');
  if (!response.ok) return null;
  return response.json();
};

export const getRecordById = async (id) => {
  const response = await fetch(`/api/records/${id}`);
  if (!response.ok) return null;
  return response.json();
};

export const createRecord = async (data) => {
  const response = await fetch('/api/records', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });
  if (!response.ok) return null;
  return response.json();
};

export const updateRecord = async (data) => {
  const { _id, ...body } = data;
  const response = await fetch(`/api/records/${_id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  if (!response.ok) return null;
  return response.json();
};

export const deleteRecord = async (id) => {
  const response = await fetch(`/api/records/${id}`, { method: 'DELETE' });
  return response.ok;
};

```

{% endcode %}

## 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="/files/kOpxykUYgIUMwjRH4Hei" alt=""><figcaption></figcaption></figure>

Mergem în folderul api, și ne asigurăm că avem următoarea structură a fișierelor:

<figure><img src="/files/YhU7M1KcxMsbaeYGWhkk" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
**Explicații**

În Next.js (App Router), structura din folderul app este bazată pe convenții. Fiecare segment de folder contribuie la construirea rutei, iar fișierul **page.jsx** este cel care definește efectiv pagina pentru ruta **/records/create**.\
De aceea, dacă vrem să afișăm formularul de creare la **/records/create**, trebuie să avem exact această structură: folderele records/create și un fișier page.jsx în interior.\
În mod similar, **page.js** definește pagina principală a aplicației (ruta /), adică view-ul afișat când accesăm Home.
{% endhint %}

```javascript
// /app/page.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.

{% code expandable="true" %}

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

'use client';

import { useState, useEffect } from 'react';
import Link from 'next/link';
import { getRecords, deleteRecord } from '@/utils/recordsFunctions';

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

  const fetchRecords = async () => {
    const data = await getRecords();
    if (data) {
      setRecords(data);
    }
  };

  const handleDelete = async (id) => {
    const success = await deleteRecord(id);
    if (success) {
      setRecords((prev) => prev.filter((r) => r._id !== id));
    } else {
      alert('Failed to delete record');
    }
  };

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

  return (
    <div className="max-w-4xl mx-auto p-8">
      <div className="flex flex-col justify-between items-center mb-8">
        <h1 className="text-3xl font-bold">Records</h1>
        <div className="flex items-center gap-3">
          <Link
            href="/contact"
            className="bg-gray-700 text-white rounded px-4 py-2 hover:bg-gray-800 transition-colors"
          >
            Contact
          </Link>
          <Link
            href="/records/create"
            className="bg-blue-600 text-white rounded px-4 py-2 hover:bg-blue-700 transition-colors"
          >
            Add Record
          </Link>
        </div>
      </div>

      {records.length === 0 ? (
        <p className="text-gray-500">No records found.</p>
      ) : (
        <div className="flex flex-col gap-4">
          {records.map((record) => (
            <div
              key={record._id}
              className="border rounded-lg p-4 flex justify-between items-start shadow-sm"
            >
              <div>
                <h2 className="text-xl font-semibold">{record.name}</h2>
                {record.type && (
                  <p className="text-gray-500 text-sm">{record.type}</p>
                )}
                {record.description && (
                  <p className="mt-1 text-gray-700">{record.description}</p>
                )}
              </div>

              <div className="flex gap-2">
                <Link
                  href={`/records/edit?id=${record._id}`}
                  className="bg-yellow-400 text-white rounded px-3 py-1 hover:bg-yellow-500 transition-colors text-sm"
                >
                  Edit
                </Link>
                <button
                  onClick={() => handleDelete(record._id)}
                  className="bg-red-500 text-white rounded px-3 py-1 hover:bg-red-600 transition-colors text-sm"
                >
                  Delete
                </button>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

export default MainPage;
```

{% endcode %}

{% 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 pentru a apela metoda **fetchRecords** care cere la baza de date lista cu toate entry-urile.
{% endhint %}

<figure><img src="/files/S1ZjBkP1AZ0PKsYN5dl4" alt=""><figcaption></figcaption></figure>

{% 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. Actualizarea fișierului cu reguli de ESLint

Mergem în fișierul eslint.config.msj și adăugăm prima noastră regulă de ESLint, în care stabilim la nivel de proiect că este în regulă să folosim `setState` în `useEffect`.

```javascript
// eslint.config.mjs

import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";

const eslintConfig = defineConfig([
  ...nextVitals,
  {
    rules: {
      "react-hooks/set-state-in-effect": "off",
    },
  },
  // Override default ignores of eslint-config-next.
  globalIgnores([
    // Default ignores of eslint-config-next:
    ".next/**",
    "out/**",
    "build/**",
    "next-env.d.ts",
  ]),
]);

export default eslintConfig;
```

{% hint style="warning" %}
Dezactivarea regulii `react-hooks/set-state-in-effect` poate permite folosirea `setState` în `useEffect` fără avertismente, dar riscă să ascundă anti-pattern-uri sau render-uri inutile care ar putea afecta performanța sau claritatea logicii componentei dacă nu este folosită cu grijă.
{% endhint %}

## 5. 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
// /app/records/create/page.jsx

'use client';

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

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

  const onSubmit = async (data) => {
    const response = await createRecord(data);
    if (response) {
      router.push('/');
    } else {
      alert('Failed to create record');
    }
  };

  return (
    <div className="max-w-2xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-8">Create Record</h1>
      <RecordForm data={recordDefaultValues} onSubmit={onSubmit} />
    </div>
  );
};

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)

`useRouter()` în Next.js este un **hook** care ne permite să controlăm navigarea din cod, direct dintr-o componentă React.
{% 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 prop unei componente copil, cu scopul ca aceasta 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**.

{% code expandable="true" %}

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

'use client';

import { useState } from 'react';

const RecordForm = ({ data, onSubmit }) => {
  const [formData, setFormData] = useState({ name: '', type: '', description: '', ...data });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData((prev) => ({ ...prev, [name]: value }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    onSubmit(formData);
  };

  return (
    <form onSubmit={handleSubmit} className="flex flex-col gap-4">
      <div className="flex flex-col gap-1">
        <label htmlFor="name" className="font-medium">
          Name
        </label>
        <input
          id="name"
          name="name"
          type="text"
          value={formData.name}
          onChange={handleChange}
          className="border rounded px-3 py-2"
          required
        />
      </div>

      <div className="flex flex-col gap-1">
        <label htmlFor="type" className="font-medium">
          Type
        </label>
        <input
          id="type"
          name="type"
          type="text"
          value={formData.type}
          onChange={handleChange}
          className="border rounded px-3 py-2"
        />
      </div>

      <div className="flex flex-col gap-1">
        <label htmlFor="description" className="font-medium">
          Description
        </label>
        <textarea
          id="description"
          name="description"
          value={formData.description}
          onChange={handleChange}
          className="border rounded px-3 py-2"
          rows={3}
        />
      </div>

      <button
        type="submit"
        className="bg-blue-600 text-white rounded px-4 py-2 hover:bg-blue-700 transition-colors"
      >
        {formData._id ? 'Update' : 'Create'}
      </button>
    </form>
  );
};

export default RecordForm;
```

{% endcode %}

{% 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 %}

## 6. Editarea unui entry

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

{% code expandable="true" %}

```javascript
// /app/records/edit/page.jsx

"use client";

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

const EditContent = () => {
  const router = useRouter();
  const searchParams = useSearchParams();
  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 id = searchParams.get("id");
    if (!id) {
      router.push("/");
      return;
    }
    getRecord(id);
  }, []);

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

  return (
    <div className="max-w-2xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-8">Edit Record</h1>
      <RecordForm data={entry} onSubmit={onSubmit} />
    </div>
  );
};

const Edit = () => {
  return (
    <Suspense fallback={<Spinner />}>
      <EditContent />
    </Suspense>
  );
};

export default Edit;
```

{% endcode %}

{% 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 recordului 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 recordul 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.

În Next App Router, **Suspense e important la prerender/build** pentru lucruri care depind de request sau client state (ex: useSearchParams în client). Fără boundary, Next nu știe unde să „taie” partea dinamică și poate da eroare la build. Cu boundary, Next păstrează shell-ul static și tratează doar subarborele respectiv ca dinamic.
{% 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;
```
