Integrasi Konten Strapi ke dalam Frontend React – Bag. 3

How can we help?
< All Topics
Print

Integrasi Konten Strapi ke dalam Frontend React – Bag. 3

Setelah pada tutorial sebelumnya kita memahami cara mengelola data konten di backend Strapi, langkah selanjutnya adalah menampilkan data ini di bagian frontend aplikasi kita.

Untuk bagian frontend, akan digunakan React dengan Vite sebagai alat pengembangannya, dan untuk styling setiap elemennya, akan digunakan class dari Tailwind CSS. Pemilihan ini bertujuan untuk memudahkan dan membuat tampilan situs web kita lebih fleksibel.

Bagi Anda yang mungkin menggunakan frontend lain, pastinya tidak menjadi masalah. Namun, dalam tutorial ini disarankan untuk mengikuti langkah-langkah yang diberikan, agar Anda memahami konsepnya terlebih dahulu. Setelah itu, Anda dapat mengaplikasikan konsep ini ke teknologi lain yang Anda gunakan.

Beberapa aktifitas pengolahan data dari Strapi yang akan kita lakukan dalam frontend ini antara lain:

  • Menampilkan data semua artikel
  • Menampilkan data salah satu artikel berdasarkan ID
  • Implementasi autentikasi melalui proses registrasi, login, dan logout

Tentunya, Anda dapat menjelajahi lebih jauh dari studi kasus sederhana ini sesuai dengan kebutuhan masing-masing di masa mendatang.

Menjalankan Proyek Vite

Jika Anda belum memiliki proyek Vite, Anda perlu membuatnya terlebih dahulu. Beruntungnya, Tailwind CSS menyediakan panduan langkah demi langkah tentang cara mengintegrasikannya ke dalam proyek Vite. Anda bisa melihat langkah-langkahnya di halaman berikut: https://tailwindcss.com/docs/guides/vite

Namun, di sini saya telah menyediakan repositori di GitHub saya terkait template website “Mini Article” yang akan kita gunakan di dalam tutorial ini.

Hal ini sejalan dengan konsep pengelolaan konten dalam Strapi yang telah dijelaskan dalam tutorial sebelumnya, bahwa kita akan membangun sebuah situs Artikel.

Akses tautan berikut dan lakukan “clone” pada repositori tersebut.

Sekarang kita sudah bisa mulai menjalankan proyek Vite dengan perintah berikut:

				
					npm install
npm run dev -- --host 10.20.0.44 <-- [IP Private VM kita]
				
			

Pertama-tama mari kita panggil kembali endpoint API dari artikel kita untuk memastikan bahwa data dapat diakses dengan normal.

				
					GET http://163.53.195.233:1337/api/articles?populate=*
				
			

Terpantau hasil respon API menunjukkan bahwa data telah berhasil diambil dengan sukses.

Mengambil Data dari Strapi ke dalam Proyek Vite

Pertama-tama, kita memerlukan fungsi untuk mengambil data dari Strapi. Dalam konteks ini, kita akan menggunakan sebuah hook. Hook ini akan mengatur proses pengambilan data dari API.

Buat sebuah folder bernama ‘hooks’ di dalam direktori proyek Anda, lalu buatlah sebuah hook baru dengan nama semisal 'useFetch.js'.

Kemudian masukkan skrip seperti di bawah:

// useFetch.js

import { useEffect, useState } from 'react'; // Import useEffect and useState hooks dari React's library

// Deklarasikan useFetch sebagai custom React hook untuk mengambil data dari Endpoint URL Strapi
// Parameter URL akan diteruskan dari komponen/page yang menggunakan hook ini

const useFetch = (url) => {
    const [data, setData] = useState(null); // State untuk menyimpan data yang diambil
    const [error, setError] = useState(null); // State untuk menangani error
    const [loading, setLoading] = useState(true); // State untuk mengatur status loading

    // Definisikan fungsi fetchData untuk mengambil data menggunakan fetch
    const fetchData = async () => {
        setLoading(true); // Tunjukkan bahwa pengambilan data sedang berlangsung
        try {
            const response = await fetch(url); // Mengambil data dari URL menggunakan fetch.
            const json = await response.json(); // Mengubah respons menjadi objek JSON.
            setData(json); // Mengatur data dengan objek JSON yang diterima.
            setLoading(false); // Menghentikan status loading.
        } catch (error) {
            setError(error); // Tangani kesalahan dengan mengatur state error
            setLoading(false); // Tunjukkan pengambilan data sudah selesai (meskipun ada kesalahan)
        }
    };

    // Gunakan hook useEffect untuk memulai operasi pengambilan Data ketika ada URL yg berubah
    // Jalankan efek setiap kali ada permintaan baru terkait pengambilan data melalui parameter url
    useEffect(() => {
        fetchData(); // Panggil fungsi fetchData untuk menjalankan isi dari fungsinya
    }, [url]);

    // Kirimkan status loading, error, dan data ke page/komponen yang menggunakan hook ini
    return { loading, error, data };
};

export default useFetch;

Di dalam skrip tersebut, kita membuat fungsi React hook kustom bernama useFetch. Hook ini akan berguna dalam proses pengambilan data berdasarkan parameter URL Endpoint-nya. Untuk melakukannya kita menggunakan dua hook React yang penting, yaitu useState and useEffect:

  • useEffect digunakan untuk memantau perubahan pada nilai-nilai tertentu, seperti perubahan URL dalam konteks pengambilan data. Yang memungkinkan kita menjalankan kode ketika terjadi perubahan yang kita inginkan, seperti permintaan data baru.
  • Sedangkan useState, dalam konteks pengambilan data dari URL, akan membantu kita menyimpan hasil permintaan data, status loading, dan penanganan kesalahan.

Dan kita menggunakan fetch untuk melakukan permintaan HTTP, termasuk pengambilan data dari URL.

Dalam hook ini, terdapat fungsi asinkron yang disebut fetchData yang mengurus seluruh proses pengambilan data. Dengan penggunaan async, kita memanfaatkan await untuk memastikan agar fungsi menunggu respons dari URL dahulu. Sambil terus menunjukkan loading state jika data belum didapat.

Di dalam Vite, terdapat 'App.jsx' dimana merupakan file utama atau entri dari aplikasi React.

Pada dasarnya, file ini merupakan titik awal dari aplikasi React. Di dalamnya, kita dapat mengatur komponen-komponen utama, konfigurasi, dan struktur dasar dari aplikasi React, salah satu dari beberapa hal yang biasanya diatur dalam 'App.jsx' adalah Routing melalui bantuan library routing seperti react-router-dom.

Kita akan mengambil konten data artikel dari Strapi melalui file 'App.jsx'. Modifikasi beberapa skrip pada halaman ini untuk melakukan pemanggilan data dari API Strapi, kemudian kirimkan data tersebut ke beberapa halaman yang kita inginkan:

// App.jsx

import { HomePage, ArticleContentPage, UserRegistrationPage, UserLoginPage, CategoriesPage } from "./pages";
import { Routes, Route } from "react-router-dom";
import useFetch from "./hooks/useFetch"; // Pertama, impor 'useFetch' hook ke dalam komponen App kita

export default function App() {

  // Dalam fungsi useFetch, kita mendefinisikan parameter URL sesuai dengan endpoint Strapi yang diinginkan.
  // Di sini, saya memuat semua konten Artikel.
  let { loading: articleLoading, data: articleData, error: articleError } = useFetch('http://163.53.195.233:1337/api/articles?populate=*');

	// Tampilkan animasi "Loading.." jika data masih dimuat
	if (articleLoading) {
    return (
      <div className="fixed inset-0 flex items-center justify-center z-50 bg-opacity-50 bg-gray-800">
        <div className="animate-spin rounded-full h-16 w-16 border-t-4 border-blue-500 border-opacity-75"></div>
      </div>
    );
  }
// Tampilkan pesan "Error!" jika terjadi kesalahan dalam pengambilan data
  if (articleError) {
    return (
    <div className="fixed inset-0 flex items-center justify-center z-50 bg-opacity-50 bg-gray-800">
      <div className="bg-white p-6 rounded-md shadow-md"><p className="ml-2 text-red-500">⛔ An error occurred. Please try again later. ⛔</p></div>
    </div>
    );
  }

  // Setelah data tersedia, lewatkan nilai data artikel, yang diberi alias sebagai articleData,
  // ke halaman-halaman lain yang memerlukan data ini.
  // Saya akan menggunakan variabel "articles" untuk menyimpan data ini,
  // yang kemudian akan kita panggil di halaman-hal yang sesuai (HomePage dan ArticleContentPage).
  return (
    <div>
        <Routes>
          <Route path="/" element={<HomePage articles={articleData} />} />
          <Route path="/article/:idFromURL" element={<ArticleContentPage articles={articleData} />} />
          <Route path="/registration" element={<UserRegistrationPage />} />
		  <Route path="/login" element={<UserLoginPage />} />
          <Route path="/categories" element={<CategoriesPage />} />
        </Routes>
    </div>
  )
}

Menampilkan Data semua Artikel dari Strapi

Sekarang, mari kita buka file 'HomePage.jsx' di mana kita akan memanggil komponen Articles. Komponen ini akan menampilkan kumpulan data artikel yang telah kita peroleh sebelumnya. Tambahkan beberapa skrip pada halaman ini sesuai yang sudah saya highlight:

// HomePage.jsx

import React from 'react';
import { Navbar, Articles } from "../components";

// Komponen HomePage menerima prop articles yang akan digunakan untuk menampilkan daftar artikel.
// Prop articles diteruskan dari komponen App.jsx yang mengambil data artikel melalui useFetch hook.
const HomePage = ({ articles }) => {
  return (
    <div>
      <Navbar />
      {/* Mengirimkan prop articles ke komponen Articles untuk ditampilkan */}
      <Articles articles={articles} />
    </div>
  );
};

export default HomePage;

Sekarang, mari kita membuka komponen 'Articles.jsx'. Sesuaikan beberapa skrip menjadi seperti yang saya highlight di sini:

// Articles.jsx

import React from 'react'
import { Link } from 'react-router-dom'
import { ReactMarkdown } from 'react-markdown/lib/react-markdown'

// Menggunakan prop articles yang diterima dari parent component (App.jsx).
// Hal ini memastikan bahwa aliran data tetap terjaga
const Articles = ({articles}) => {

    // data artikel awal yang masih static
    /* const articles=[ ... ]*/

  return (
    <div className='w-full bg-[#f9f9f9] py-[50px]'>
        <div className='max-w-[1240px] mx-auto'>
            <div className='grid lg:grid-cols-3 md:grid-cols-2 sm:grid-cols-2 ss:grid-cols-1 gap-8 px-4 text-black'>
                
                {/* Kita melakukan mapping terhadap data artikel yang diberikan melalui prop articles */}
                {/* Menggunakan .map untuk mengiterasi setiap artikel dalam array */}
                
                {articles.data.map((articlesmap)=>
                    <div key={articlesmap.id} className='bg-white rounded-xl overflow-hidden drop-shadow-md'>
                        <Link to={`/article/${articlesmap.id}`}>
                        <img
                            className='h-56 w-full object-cover'
                            src={`http://163.53.195.233:1337${articlesmap.attributes.coverImage.data.attributes.url}`}
                            alt={articlesmap.attributes.title} />
                        </Link>
                        <div className='p-8'>
                        <Link to={`/article/${articlesmap.id}`}>
                            <h3 className='font-bold text-2xl my-1'>{articlesmap.attributes.title}</h3>
                        </Link>
                        <div className='flex space-x-2 mt-2'>
                            {articlesmap.attributes.categories.data.map((category) => (
                            <span
                                key={category.id}
                                className='px-2 py-1 text-sm font-medium bg-[#1A9FDA] text-gray-800 rounded'
                            >
                                {category.attributes.name}
                            </span>
                            ))}
                        </div>
                        <ReactMarkdown className='mt-2 text-gray-600 text-xl line-break'>{`${articlesmap.attributes.body.substring(0, 150)}...`}</ReactMarkdown>
                            <Link to={`/article/${articlesmap.id}`} className='text-blue-500 font-medium'>
                                Read more
                            </Link>
                        </div>
                    </div>
                )}
            </div>
        </div>
    </div>
  )
}

export default Articles

Dengan penjelasan lebih dalam dari skrip adalah sebagai berikut.

Dalam proses pemanggilan data spesifik, sangat penting bagi kita untuk memahami struktur hirarki respons API dengan baik. Berikut adalah contoh respons API yang kita gunakan:

{
    "data": [
        {
            "id": 1,
            "attributes": {
                "title": "Menginstal Strapi Headless CMS di Cloud Raya",
                "body": "Dalam era pengembangan web yang terus berkembang....",
                "createdAt": "2023-07-26T06:28:51.321Z",
                "updatedAt": "2023-08-05T15:58:19.057Z",
                "publishedAt": "2023-07-26T06:30:20.830Z",
                "categories": {
                    "data": [
                        {
                            "id": 1,
                            "attributes": {
                                "name": "Integration",
                                "createdAt": "2023-07-26T10:09:37.750Z",
                                "updatedAt": "2023-07-26T10:09:45.792Z",
                                "publishedAt": "2023-07-26T10:09:45.789Z"
                            }
                        }
                    ]
                },
                "coverImage": {
                    "data": {
                        "id": 1,
                        "attributes": {
                            "name": "cover1.jpg",
                            "url": "/uploads/cover1.jpg",
                            {...}
                        }
                    }
                }
            }
        },

Karena dalam respons API ini, semua data yang kita butuhkan untuk artikel terletak di dalam hirarki “data”, maka kita menulisnya seperti ini:

// Articles.jsx

/*...*/
{articles.data.map((articlesmap)=>
/*...*/

Dalam potongan skrip di atas:

  • articles adalah prop yang mengandung respons data dari API Strapi yang kita panggil.
  • data adalah array yang berisi data artikel dari respons API.
  • articlesmap adalah prop yang mewakili setiap elemen dalam array data artikel dari respons API.

Tujuan dari skrip ini adalah untuk melakukan pemetaan terhadap hasil panggilan API yang telah kita lakukan sebelumnya, serta memilih data yang spesifik dalam hirarki “data”.

Selanjutnya, kita hanya perlu menyesuaikan data yang ingin ditampilkan. Sebagai contoh, untuk menampilkan gambar sampul artikel, hirarkinya akan menjadi seperti berikut:

// Articles.jsx

/*...*/
<img src={`http://163.53.195.233:1337${articlesmap.attributes.coverImage.data.attributes.url}`} />
/*...*/

Sama halnya dengan pemanggilan hirarki yang berada di dalam satu tingkat lebih dalam, kita dapat menggunakan tanda titik (.) untuk merujuk pada hirarki tersebut. Sebagai contoh, untuk menampilkan data kategori berdasarkan respons API ini, susunan skripnya akan menjadi seperti berikut:

// Articles.jsx

/*...*/
{articlesmap.attributes.categories.data.map((category) => (
  <span>
    {category.attributes.name}
  </span>
))}
/*...*/

Menampilkan Data Artikel Berdasarkan ID

Oke, mari kita cek kembali skrip pada 'App.jsx' yang mengatur masalah pengambilan ID dengan menggunakan React Router ini.

// App.jsx

/*...*/
<Route path="/article/:idFromURL" element={<ArticleContentPage articles={articleData} />} />
/*...*/

path="/article/:idFromURL" menunjukkan bahwa kita sedang menetapkan rute untuk halaman artikel yang menerima suatu nilai ID sebagai parameter.

:idfromURL adalah salah satu contoh parameter dinamis yang umum digunakan dalam routing untuk membuat rute yang spesifik. Nilai :idfromURL ini akhirnya akan didapat pada tautan setiap artikel yang sudah kita atur sebelumnya:

// Articles.jsx

/*...*/
<Link to={`/article/${articlesmap.id}`}>...</Link>
/*...*/

Sekarang, mari buka file 'ArticleContentPage.jsx' terlebih dahulu untuk mengalirkan data dari articles ke komponen ArticleContent yang akan menampilkan detail artikel. Sesuaikan skrip menjadi seperti berikut:

// ArticleContentPage.jsx

import React from 'react'
import { Navbar, ArticleContent } from "../components"

const ArticleContentPage = ({articles}) => {
  return (
    <div>
         <Navbar />
         <ArticleContent articles={articles}/>
    </div>
  )
}

export default ArticleContentPage

Kemudian buka 'ArticleContent.jsx' dan sesuaikan skripnya menjadi seperti berikut:

// ArticleContent.jsx

import React from 'react'
import { useParams } from 'react-router-dom'
import { ReactMarkdown } from 'react-markdown/lib/react-markdown'

const ArticleContent = ({ articles }) => {
  // useParams() sebagai hook yang disediakan oleh React Router akan membaca parameter dari URL dan menyimpannya di dalam variabel idFromURL.
  const { idFromURL } = useParams()

  /*const articles=[...] */

  /*
  Membuat array baru dengan memfilter data artikel berdasarkan artikel ID yang diambil dari variabel idFromURL.
  Jika variabel idFromURL cocok dengan ID artikel yang tersedia, maka gunakan datanya.
  Jika tidak cocok, tampilkan pesan error "Article not found".
  */
  let articlemap = {};
  let array = articles.data.filter((articlemap) => articlemap.id == idFromURL);
  if (array.length > 0) {
    articlemap = array[0];
  } else {
    return (
      <div className="flex items-center justify-center h-screen">
        <p>⛔ Article not found. ⛔</p></div>
    );
  }


  return (

    <div className='w-full pb-10 bg-[#f9f9f9]'>
      <div className='max-w-7xl mx-auto px-4 py-12 sm:px-6 lg:px-8'>
        <div className='grid lg:grid-cols-3 gap-8'>
          <div className='col-span-2 bg-white shadow rounded-lg overflow-hidden'>
            <img className='h-[300px] w-full object-cover' src={`http://163.53.195.233:1337${articlemap.attributes.coverImage.data.attributes.url}`} />
            <div className='p-6'>
              <h1 className='text-3xl font-semibold'>{articlemap.attributes.title}</h1>
              <div className='flex space-x-2 mt-2'>
                {articlemap.attributes.categories.data.map((categorymap) => (
                  <span key={categorymap.id} className='px-2 py-1 text-sm font-medium bg-[#1A9FDA] text-gray-800 rounded'>
                    {categorymap.attributes.name}
                  </span>
                ))}
              </div>
              <ReactMarkdown className='mt-2 text-gray-500 text-justify' >{articlemap.attributes.body}</ReactMarkdown>
            </div>
          </div>
          <div className='items-center w-full bg-white rounded-xl drop-shadow-md py-5 max-h-[250px]'>
            
              <div>
                <img className='p-2 w-32 h-32 rounded-full mx-auto object-cover' src={`http://163.53.195.233:1337${articlemap.attributes.authorImage.data.attributes.url}`} />
                <h1 className='font-bold text-2xl text-center text-gray-900 pt-3'>{articlemap.attributes.authorName}</h1>
                <p className='text-center text-gray-900 font-medium'>{articlemap.attributes.authorDesc}</p>
              </div>
            
          </div>
        </div>
      </div>
    </div>
  )
}

export default ArticleContent

Registrasi Pengguna

Berikut ini adalah implementasi singkat dalam melakukan registrasi pengguna di Strapi menggunakan React.

Untuk fungsi esensial yang kita butuhkan adalah sebagai berikut:

// RegistrationForm.jsx

import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';

const RegistrationForm = () => {
   // State untuk menyimpan data formulir
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
  });

  const [isSuccess, setIsSuccess] = useState(false);
  const [isError, setIsError] = useState(false);
  const [errorMessage, setErrorMessage] = useState('');

  // Menggunakan useNavigate dari react-router-dom untuk navigasi halaman
  const navigate = useNavigate();

  // Mengatur perubahan input dalam formulir
  const handleInputChange = (event) => {
    const { name, value } = event.target;
    setFormData((prevData) => ({
      ...prevData, // Menyalin nilai sebelumnya dari state formData
      [name]: value, // Memperbarui nilai yang sesuai dengan input yang diubah
    }));
  };

  // Menghandle pengiriman formulir
  const handleSubmit = async (event) => {

    // Mencegah perilaku default browser yang biasanya akan memuat ulang halaman atau berpindah ke halaman lain saat tombol "Register" ditekan
    // Kita akan mengambil alih pengiriman data formulir dan mengontrolnya dengan menggunakan fungsi handleSubmit.
    event.preventDefault();

    // Mengirim data formulir ke server menggunakan metode POST
    const response = await fetch('http://163.53.195.233:1337/api/auth/local/register', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(formData),
    });

    const responseData = await response.json();

    // Jika permintaan berhasil (ditandai dengan terbuatnya JWT token), setIsSucess menjadi true.
    if (responseData.jwt) {
      setIsSuccess(true);
      setIsError(false);
    } else {
      // Menangani kesalahan jika terjadi masalah selama proses pengiriman
      setIsSuccess(false);
      setIsError(true);
      setErrorMessage(responseData.error.message);
    }
  };
  // navigasikan ke halaman utama ketika klik tombol close.
  const handleModalClose = () => {
    setIsSuccess(false);
    setIsError(false);
    if (isSuccess) {
      navigate('/');
    }
  };

  return (
    <div className="flex justify-center items-center h-screen bg-gray-100">
      <div className="w-full max-w-md p-6 bg-white rounded-lg shadow-md">
        <h2 className="text-2xl font-semibold mb-4 text-center">Registration</h2>
        <form onSubmit={handleSubmit}>
          <div className="mb-4">
            <label htmlFor="username" className="block text-sm font-medium">
              Username
            </label>
            <input
              type="text"
              id="username"
              name="username"
              value={formData.username}
              onChange={handleInputChange}
              className="mt-1 p-2 w-full border rounded focus:outline-none focus:border-blue-400"
              required
            />
          </div>
          <div className="mb-4">
            <label htmlFor="email" className="block text-sm font-medium">
              Email
            </label>
            <input
              type="email"
              id="email"
              name="email"
              value={formData.email}
              onChange={handleInputChange}
              className="mt-1 p-2 w-full border rounded focus:outline-none focus:border-blue-400"
              required
            />
          </div>
          <div className="mb-4">
            <label htmlFor="password" className="block text-sm font-medium">
              Password
            </label>
            <input
              type="password"
              id="password"
              name="password"
              value={formData.password}
              onChange={handleInputChange}
              className="mt-1 p-2 w-full border rounded focus:outline-none focus:border-blue-400"
              required
            />
          </div>
          <button
            type="submit"
            className="w-full py-2 px-4 bg-[#202E60] rounded-md hover:bg-[#30448c] hover:text-white"
          >
            Register
          </button>
        </form>
        <div className='py-5 border-t border-gray-300 mt-5'>
          <p class="text-sm text-center text-gray-400">Already have an account? <Link to={`/login`} class="text-indigo-400 focus:outline-none focus:underline focus:text-indigo-500 dark:focus:border-indigo-800">Login</Link>.</p>
        </div>
      </div>
      {isSuccess && (
        <div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
          <div className="bg-white p-6 rounded shadow-md text-center">
            <p className="text-green-500 font-semibold mb-2">Registration successful</p>
            <button
              onClick={handleModalClose}
              className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 focus:outline-none"
            >
              Close
            </button>
          </div>
        </div>
      )}
      {isError && (
        <div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
          <div className="bg-white p-6 rounded shadow-md text-center">
            <p className="text-red-500 font-semibold mb-2">{errorMessage}</p>
            <button
              onClick={handleModalClose}
              className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 focus:outline-none"
            >
              Close
            </button>
          </div>
        </div>
      )}
    </div>
  );
};

export default RegistrationForm;

Login Pengguna

Proses login mengikuti alur yang hampir mirip dengan proses registrasi, namun perlu dilakukan penyesuaian pada URL endpoint-nya:

// LoginForm.jsx

import React, { useState } from 'react';
import { useNavigate, Link, Route } from 'react-router-dom';

const LoginForm = () => {
  const [formData, setFormData] = useState({
    identifier: '',
    password: '',
  });

  const [isSuccess, setIsSuccess] = useState(false);
  const [isError, setIsError] = useState(false);
  const [errorMessage, setErrorMessage] = useState('');

  const navigate = useNavigate();

  const handleInputChange = (event) => {
    const { name, value } = event.target;
    setFormData((prevData) => ({
      ...prevData,
      [name]: value,
    }));
  };

  const handleSubmit = async (event) => {
    event.preventDefault();

    const response = await fetch('http://163.53.195.233:1337/api/auth/local', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(formData),
    });

    const responseData = await response.json();

    if (responseData.jwt) {
      setIsSuccess(true);
      setIsError(false);
    } else {
      setIsSuccess(false);
      setIsError(true);
      setErrorMessage(responseData.error.message);
    }
  };

  const handleModalClose = () => {
    setIsSuccess(false);
    setIsError(false);
    if (isSuccess) {
      navigate('/');
    }
  };

  return (
    <div className="flex justify-center items-center h-screen bg-gray-100">
      <div className="w-full max-w-md p-6 bg-white rounded-lg shadow-md">
        <h2 className="text-2xl font-semibold mb-4 text-center">Login</h2>
        <form onSubmit={handleSubmit}>
          <div className="mb-4">
            <label htmlFor="identifier" className="block text-sm font-medium">
              Username
            </label>
            <input
              type="text"
              id="identifier"
              name="identifier"
              value={formData.username}
              onChange={handleInputChange}
              className="mt-1 p-2 w-full border rounded focus:outline-none focus:border-blue-400"
              required
            />
          </div>
          <div className="mb-4">
            <label htmlFor="password" className="block text-sm font-medium">
              Password
            </label>
            <input
              type="password"
              id="password"
              name="password"
              value={formData.password}
              onChange={handleInputChange}
              className="mt-1 p-2 w-full border rounded focus:outline-none focus:border-blue-400"
              required
            />
          </div>
          <button type="submit" className="w-full py-2 px-4 bg-[#202E60] rounded-md hover:bg-[#30448c] hover:text-white">
            Login
          </button>
        </form>
        <div className='py-5 border-t border-gray-300 mt-5'>
          <p className="text-sm text-center text-gray-400">Don't have an account yet? <Link to={`/registration`} className="text-indigo-400 focus:outline-none focus:underline focus:text-indigo-500 dark:focus:border-indigo-800">Sign up</Link>.</p>
        </div>
      </div>
      {isSuccess && (
        <div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
          <div className="bg-white p-6 rounded shadow-md text-center">
            <p className="text-green-500 font-semibold mb-2">Login success</p>
            <button
              onClick={handleModalClose}
              className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 focus:outline-none"
            >
              Close
            </button>
          </div>
        </div>
      )}
      {isError && (
        <div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
          <div className="bg-white p-6 rounded shadow-md text-center">
            <p className="text-red-500 font-semibold mb-2">{errorMessage}</p>
            <button
              onClick={handleModalClose}
              className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 focus:outline-none"
            >
              Close
            </button>
          </div>
        </div>
      )}
    </div>
  )
};

export default LoginForm;

Setiap kali login berhasil, jika kita aktifkan console.log(responseData.jwt) pada skrip, akan ditampilkan JWT (Javascript Web Token) sesuai pembahasan pada artikel sebelumnya.

JWT ini yang akan kita simpan dalam cookie pada browser untuk menjaga kelangsungan sesi.

Aktifitas lanjutan

▶ Manajemen Sesi Login dengan Cookie

Untuk memastikan penyimpanan sesi login berjalan dengan baik, salah satu metodenya adalah dengan menyimpan token JWT di dalam cookie.
Dalam manajemen cookie, ada berbagai cara, seperti menggunakan document.cookie atau mengelola cookie secara manual di server. Namun, untuk kepraktisan dan kejelasan dalam penulisan skrip, kita dapat memanfaatkan js-cookie, sebuah library yang membantu dalam pengaturan cookie.

Pertama-tama, kita perlu menginstal library js-cookie dengan perintah:

				
					npm install js-cookie
				
			

Setelah instalasi selesai, kita dapat mengimpor library ini di dalam file 'LoginForm.jsx' sebelumnya:

// LoginForm.jsx

/*...*/
import Cookies from 'js-cookie';
/*...*/

Langkah selanjutnya adalah menambahkan konfigurasi untuk menyimpan cookie saat login berhasil.

// LoginForm.jsx

/*...*/
// Jika permintaan berhasil (ditandai dengan terbuatnya JWT token), navigasikan ke halaman utama
if (responseData.jwt) {
  // Set JWT token dengan js-cookie
	Cookies.set('jwt', responseData.jwt, {
		expires: 30 / (60 * 24), // menghitung waktu kedaluwarsa cookie 30 menit dalam bentuk fraksi dari 1 hari (24 jam), di mana 1 hari terdiri dari 60 * 24 menit.
	});
	navigate('/');
}
/*...*/

Setelah itu, kita dapat mencoba untuk melakukan login dan memeriksa cookie pada browser. Anda akan melihat bahwa token JWT telah tersimpan dengan nama 'jwt'.

Walaupun langkah-langkah ini relatif sederhana, penting untuk mengingat bahwa pengaturan cookie bisa lebih kompleks tergantung pada kebutuhan aplikasi Anda. Beberapa opsi cookie seperti domain, path, httponly, dan secure mungkin perlu disesuaikan. Pastikan untuk merujuk pada dokumentasi js-cookie atau panduan umum tentang cookie untuk memastikan pengaturan yang tepat.

▶ Tampilkan Tombol Logout setelah Berhasil Login

Kita bisa menggunakan Cookies.get('jwt') untuk memeriksa apakah token ada dalam cookie. Jika ada, tombol login dan register akan digantikan dengan tombol logout.

//Navbar.jsx

/*...*/
import Cookies from 'js-cookie'; // Import js-cookie library

const Navbar = () => {
/*...*/
  const handleLogout = () => {
    Cookies.remove('jwt'); // Menghapus token pada logout
  };

  return (
    <div>
      <div>
        {/*...*/}
        {Cookies.get('jwt') ? ( // Cek apakah token ada dalam cookie
          <div>
            <button onClick={handleLogout}>Logout</button>
          </div>
        ) : (
          <div>
            <Link to={`/login`}><button>Login</button></Link>
            <Link to={`/registration`}><button>Register</button></Link>
          </div>
        )}
        {/*...*/}
      </div>
    </div>
  )
}

export default Navbar;

Kesimpulan

Dalam tutorial ini, kita telah memahami cara mengelola data konten dari backend Strapi dan kemudian menampilkannya pada frontend aplikasi.

Adapun tutorial series lainnya yang dapat melengkapi pemahaman Anda tentang pengembangan dengan Strapi dapat ditemukan sebelum atau sesudah artikel ini, atau Anda dapat mengeksplorasi berbagai tutorial menarik lainnya di halaman Knowledge Base (KB) Cloud Raya.

Table of Contents

Leave a Reply

Your email address will not be published. Required fields are marked *

Post comment

Ready, Set, Cloud

Ready, Set, Cloud