Logo Universitas

Universitas ASA Indonesia

Program Studi: Teknologi Informasi

Mata Kuliah: Pengembangan Aplikasi Web Lanjut

SKS: 3 (2 teori, 1 praktikum)

Dosen Pengampu: Istiqomah Sumadikarta, S.T., M.Kom.

Beranda Mundur Maju

Pertemuan 8: Implementasi Halaman Admin #2

Membuat Component Pagination


Pagination merupakan teknik untuk membagi sebuah data menjadi beberapa halaman yang terpisah, sehingga pengguna tidak bingung dengan banyaknya informasi yang ditampilkan.

Contoh sederhananya adalah buku, dimana buku memuat sumber informasi yang sangat banyak dan untuk mempermudah pembaca, maka akan dipisah-pisah menjadi beberapa halaman.

Di dalam website juga seperti itu, kita juga membutuhkan pagination untuk membatasi data yang ditampilkan per-halaman, tujuannya agar tidak membingungkan dengan banyaknya infomasi yang ditampilkan.

Karena pagination akan sering digunakan di dalam website, maka kita akan membuatnya menjadi sebuah component, sehingga kita dapat menggunakan berulang-ulang (reusable) di dalam component lain.

Silahkan buat folder baru dengan nama utilities di dalam folder src/components. Dan di dalam folder utilities silahkan buat file baru dengan nama Pagination.jsx, kemudian masukkan kode berikut ini di dalamnya.

//import pagination
import Pagination from "react-js-pagination";

function PaginationComponent(props) {

    return (
        props.total > 0 && (
            <Pagination
                innerClass={`pagination justify-content-${props.position} mb-0`}
                activePage={props.currentPage}
                activeClass="page-item active"
                itemsCountPerPage={props.perPage}
                totalItemsCount={props.total}
                onChange={props.onChange}
                itemClasss="page-item"
                linkClass="page-link"
            />
        )
    )
}

export default PaginationComponent;

Dari penambahan kode di atas, pertama kita import package React Js Pagination.

//import pagination
import Pagination from "react-js-pagination";

Setelah itu, di dalam function component PaginationComponent kita berikan parameter berupa pops.

function PaginationComponent(props) {

	//...

}

Di dalam method return kita melakukan sebuah kondisi, jika nilai props dari total di atas 0, maka kita akan melakukan render component Pagination dari React Js Pagination.

props.total > 0 && (

	//...
	
)

Di dalam component Pagination, kita membuat beberapa konfigurasi, seperti :

<Pagination
   innerClass={`pagination justify-content-${props.position} mb-0`}
   activePage={props.currentPage}
   activeClass="page-item active"
   itemsCountPerPage={props.perPage}
   totalItemsCount={props.total}
   onChange={props.onChange}
   itemClasss="page-item"
   linkClass="page-link"
/>

Konfigurasi Private Route Categories Index


Pada materi kali ini kita akan belajar membuat konfigurasi private route untuk menampilkan halaman categories index. Sebelum membuat konfigurasi private route, sama seperti sebelum-sebelumnya kita akan membuat view/component-nya terlebih dahulu.

Langkah 1 - Membuat View/Component Categories Index

Sekarang kita akan membuat view/component untuk halaman categories index. Silahkan buat folder baru dengan nama categories di dalam folder src/pages/admin. Dan di dalam folder categories silahkan buat file baru dengan nama Index.jsx, kemudian masukkan kode berikut ini di dalamnya.

//import react  
import React from "react";

//import layout admin
import LayoutAdmin from "../../../layouts/Admin";

function CategoriesIndex() {

    return(
        <React.Fragment>
            <LayoutAdmin>
                <div className="row mt-4">
                    <div className="col-12">
                        <div className="card border-0 rounded shadow-sm border-top-success">
                            <div className="card-header">
                                <span className="font-weight-bold"><i className="fa fa-folder"></i> CATEGORIES</span>
                            </div>
                        </div>
                    </div>
                </div>
            </LayoutAdmin>
        </React.Fragment>
    )

}

export default CategoriesIndex

Dari penambahan kode di atas, pertama kita import React dari react.

//import react  
import React from "react";

Selanjutnya, karena view/component ini bagian dari halaman admin, maka kita akan tempatkan di dalam layout admin. Dan disini kita import LayoutAdmin yang sudah pernah kita buat sebelumnya.

//import layout admin
import LayoutAdmin from "../../../layouts/Admin";

Di dalam JSX kita tulis kode-kode kita di dalam LayoutAdmin. Tujuannya agar view/component di render sebagai child dari LayoutAdmin.

<LayoutAdmin>

	//...
	
</LayoutAdmin>

Langkah 2 - Konfigurasi Private Route Categories Index

Setelah berhasil membuat view/component categories index, sekarang kita lanjutkan untuk membuat konfigurasi private route-nya.

Silahkan buka file src/routes/routes.jsx, kemudian ubah semua kode-nya menjadi seperti berikut ini :

//import react router dom
import { Routes, Route } from "react-router-dom";

//=======================================================================
//ADMIN
//=======================================================================

//import view Login
import Login from '../pages/admin/Login.jsx';

//import component private routes
import PrivateRoute from "./PrivateRoutes";

//import view admin Dashboard
import Dashboard from '../pages/admin/dashboard/Index.jsx';

//import view admin categories Index
import CategoriesIndex from '../pages/admin/categories/Index.jsx';

function RoutesIndex() {
    return (
        <Routes>

            {/* route "/admin/login" */}
            <Route path="/admin/login" element={<Login />} />

            {/* private route "/admin/dashboard" */}
            <Route
                path="/admin/dashboard"
                element={
                        <PrivateRoute>
                            <Dashboard />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories" */}
            <Route
                path="/admin/categories"
                element={
                        <PrivateRoute>
                            <CategoriesIndex />
                        </PrivateRoute>
                }
            />

        </Routes>
    )
}

export default RoutesIndex

Dari perubahan kode di atas, pertama kita import view/component categories index terlebih dahulu.

//import view admin categories Index
import CategoriesIndex from '../pages/admin/categories/Index.jsx';

Setelah itu, kita buatkan konfigurasi private route-nya seperti berikut ini :

{/* private route "/admin/categories" */}
<Route
    path="/admin/categories"
    element={
    <PrivateRoute>
        <CategoriesIndex />
    </PrivateRoute>
    }
/>

Sekarang, jika kita klik menu CATEGORIES yang ada di sidebar, maka jika berhasil akan mendapatkan tampilan seperti berikut ini :

Menampilkan Data Categories


Pada materi kali ini kita akan belajar bersama-sama bagaiaman cara menampilkan data categories dari Rest API. Tidak hanya menampilkan data saja, kita juga akan belajar bagaimana cara membuat fitur pencarian, pagination dan delete data.

Langkah 1 - Menampilkan Data Categories

Pertama, kita akan belajar menampilkan data categories terlebih dahulu. Dan disini kita akan melakukan penambahan kode di dalam file yang sudah pernah kita buat sebelumnya. Silahkan buka file src/pages/admin/categories/Index.jsx, kemudian ubah kode-nya menjadi seperti berikut ini :

//import react  
import React, { useState, useEffect } from "react";

//import layout admin
import LayoutAdmin from "../../../layouts/Admin";

//import BASE URL API
import Api from "../../../api";

//import js cookie
import Cookies from "js-cookie";

function CategoriesIndex() {

	//title page
    document.title = "Categories - Administrator Travel GIS";

    //state posts
    const [categories, setCategories] = useState([]);

    //state currentPage
    const [currentPage, setCurrentPage] = useState(1);

    //state perPage
    const [perPage, setPerPage] = useState(0);

    //state total
    const [total, setTotal] = useState(0);

    //token
    const token = Cookies.get("token");

    //function "fetchData"
    const fetchData = async () => {

        //fetching data from Rest API
        await Api.get('/api/admin/categories', {
            headers: {
                //header Bearer + Token
                Authorization: `Bearer ${token}`,
            }
        })
        .then(response => {
            //set data response to state "categories"
            setCategories(response.data.data.data);

            //set currentPage
            setCurrentPage(response.data.data.current_page);

            //set perPage
            setPerPage(response.data.data.per_page);

            //total
            setTotal(response.data.data.total);
        });
    };

    //hook
    useEffect(() => {
        //call function "fetchData"
        fetchData();

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    return(
        <React.Fragment>
            <LayoutAdmin>
                <div className="row mt-4">
                    <div className="col-12">
                        <div className="card border-0 rounded shadow-sm border-top-success">
                            <div className="card-header">
                                <span className="font-weight-bold"><i className="fa fa-folder"></i> CATEGORIES</span>
                            </div>
                            <div className="card-body">
                                <div className="table-responsive">
                                    <table className="table table-bordered table-striped table-hovered">
                                        <thead>
                                        <tr>
                                            <th scope="col">No.</th>
                                            <th scope="col">Image</th>
                                            <th scope="col">Category Name</th>
                                            <th scope="col">Actions</th>
                                        </tr>
                                        </thead>
                                        <tbody>
                                        {categories.map((category, index) => (
                                            <tr key={index}>
                                                <td className="text-center">{++index + (currentPage-1) * perPage}</td>
                                                <td className="text-center">
                                                    <img src={category.image} alt="" width="50" />
                                                </td>
                                                <td>{category.name}</td>
                                                <td className="text-center"></td>
                                            </tr>
                                        ))}
                                        </tbody>
                                    </table>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </LayoutAdmin>
        </React.Fragment>
    )

}

export default CategoriesIndex

Dari perubahan kode di atas, pertama kita import 2 hook dari react, yaitu useState dan useEffect.

//import react  
import React, { useState, useEffect } from "react";

Karena akan melakukan HTTP request, maka kita akan import global endpoint yang sudah pernah kita buat.

//import BASE URL API
import Api from "../../../api";

Dan kita juga import package Js Cookie, karena kita akan gunakan untuk mengambil data token dari cookies browser.

//import js cookie
import Cookies from "js-cookie";

Dan di dalam function component CategoriesIndex, pertama kita atur title untuk halaman categories index.

//title page
document.title = "Categories - Administrator Travel GIS";

Setelah itu, kita menambahkan 4 state baru, yaitu :

//state posts
const [categories, setCategories] = useState([]);

//state currentPage
const [currentPage, setCurrentPage] = useState(1);

//state perPage
const [perPage, setPerPage] = useState(0);

//state total
const [total, setTotal] = useState(0);

State categories akan digunakan untuk menyimpan response data list categories. State currentPage digunakan untuk menyimpan nilai pagination yang sedang aktif. State perPage merupakan nilai jumlah data yang ditampilkan perhalaman dan untuk State total merupakan nilai total dari data categories.

Setelah itu, kita buat 1 variable lagi dengan nama token yang mana isinya adalah token yang ada di dalam cookies browser.

//token
const token = Cookies.get("token");

Kemudian kita buat function yang bernama fetchData dengan jenis asynchronus. Method ini akan kita gunakan untuk fetching data ke dalam Rest API.

//function "fetchData"
const fetchData = async () => {

	//...
	
}

Di dalam function fetchDara, pertama-tama kita melakukan fetching ke dalam endpoint /api/admin/categories dengan mengirimkan sebuah headers Authorization yang berisi Berare + Token.

//fetching data from Rest API
await Api.get('/api/admin/categories', {
    headers: {
        //header Bearer + Token
        Authorization: `Bearer ${token}`,
    }
})

Jika proses fetching berhasil dilakukan, maka kita akan assign response data dari Rest API ke dalam state yang sudah kita buat di atas.

.then(response => {
    //set data response to state "categories"
    setCategories(response.data.data.data);

    //set currentPage
    setCurrentPage(response.data.data.current_page);

    //set perPage
    setPerPage(response.data.data.per_page);

    //total
    setTotal(response.data.data.total);
});

Dan agar function fetchData dapat dijalankan saat component diload, maka kita perlu memanggilnya di dalam hook useEffect.

//hook
useEffect(() => {
    //call function "fetchData"
    fetchData();

    // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Dan sekarang kita tinggal menampilkan datanya di dalam JSX. Kurang lebih seperti berikut ini :

{categories.map((category, index) => (
	
	//...
	
))}

Di atas, kita melakukan perulangan state categories menggunakan function bawaan dari Javascript, yaitu map.

Sekarang, jika kita reload/refresh halaman categoris, maka kita akan mendapatkan hasil kurang lebih seperti berikut ini :

CATATAN : jika belum muncul data apapun, silahkan periksa di dalam database dan pastikan sudah memiliki data categories.

Langkah 2 - Membuat Fitur Pencarian

Setelah berhasil menampilkan data categories, sekarang kita lanjutkan untuk menambahkan fitur pencarian data. Silahkan buka file src/pages/admin/categories/Index.jsx, kemudian ubah kode-nya menjadi seperti berikut ini :

//import react  
import React, { useState, useEffect } from "react";

//import layout admin
import LayoutAdmin from "../../../layouts/Admin";

//import BASE URL API
import Api from "../../../api";

//import js cookie
import Cookies from "js-cookie";

//import Link from react router dom
import { Link } from "react-router-dom";

function CategoriesIndex() {

	//title page
    document.title = "Categories - Administrator Travel GIS";

    //state posts
    const [categories, setCategories] = useState([]);

    //state currentPage
    const [currentPage, setCurrentPage] = useState(1);

    //state perPage
    const [perPage, setPerPage] = useState(0);

    //state total
    const [total, setTotal] = useState(0);

    //state search
    const [search, setSearch] = useState("");

    //token
    const token = Cookies.get("token");

    //function "fetchData"
    const fetchData = async (searchData) => {

        //define variable "searchQuery"
        const searchQuery = searchData ? searchData : search;

        //fetching data from Rest API
        await Api.get(`/api/admin/categories?q=${searchQuery}`, {
            headers: {
                //header Bearer + Token
                Authorization: `Bearer ${token}`,
            }
        })
        .then(response => {
            //set data response to state "categories"
            setCategories(response.data.data.data);

            //set currentPage
            setCurrentPage(response.data.data.current_page);

            //set perPage
            setPerPage(response.data.data.per_page);

            //total
            setTotal(response.data.data.total);
        });
    };

    //hook
    useEffect(() => {
        //call function "fetchData"
        fetchData();

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    //function "searchHandler"
    const searchHandlder = (e) => {
        e.preventDefault();

        //call function "fetchDataPost" with params
        fetchData(search)
    }

    return(
        <React.Fragment>
            <LayoutAdmin>
                <div className="row mt-4">
                    <div className="col-12">
                        <div className="card border-0 rounded shadow-sm border-top-success">
                            <div className="card-header">
                                <span className="font-weight-bold"><i className="fa fa-folder"></i> CATEGORIES</span>
                            </div>
                            <div className="card-body">
                                <form onSubmit={searchHandlder} className="form-group">
                                    <div className="input-group mb-3">
                                        <Link to="/admin/categories/create" className="btn btn-md btn-success"><i className="fa fa-plus-circle"></i> ADD NEW</Link>
                                        <input type="text" className="form-control" value={search} onChange={(e) => setSearch(e.target.value)} placeholder="search by category name" />
                                        <button type="submit" className="btn btn-md btn-success"><i className="fa fa-search"></i> SEARCH</button>
                                    </div>
                                </form>
                                <div className="table-responsive">
                                    <table className="table table-bordered table-striped table-hovered">
                                        <thead>
                                        <tr>
                                            <th scope="col">No.</th>
                                            <th scope="col">Image</th>
                                            <th scope="col">Category Name</th>
                                            <th scope="col">Actions</th>
                                        </tr>
                                        </thead>
                                        <tbody>
                                        {categories.map((category, index) => (
                                            <tr key={index}>
                                                <td className="text-center">{++index + (currentPage-1) * perPage}</td>
                                                <td className="text-center">
                                                    <img src={category.image} alt="" width="50" />
                                                </td>
                                                <td>{category.name}</td>
                                                <td className="text-center"></td>
                                            </tr>
                                        ))}
                                        </tbody>
                                    </table>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </LayoutAdmin>
        </React.Fragment>
    )

}

export default CategoriesIndex

Dari perubahan kode di atas, pertama kita import provider Link dari React Router DOM. Ini akan kita gunakan untuk melakukan navigasi ke tambah data category.

//import Link from react router dom
import { Link } from "react-router-dom";

Setelah itu, kita buat 1 state baru dengan nama search.

//state search
const [search, setSearch] = useState("");

Kemudian kita melakukan sedikit perubahan di dalam function fetchData, dimana kita menambahkan sebuah parameter searchData. Parameter tersebut akan bernilai dinamis sesuai dengan kata kunci yang diinputkan.

//function "fetchData"
const fetchData = async (searchData) => {

	//...
	
}

Di dalam function fetchData, kita membuat variable baru dengan nama searchQuery yang isinya adalah sebuah kondisi menggunakan ternary oprator.

Jika searchData memiliki value, maka akan diambil dari searchData itu sendiri, tapi jika kosong maka akan mengambil dari nilai state search.

Setelah itu, kita buat varibale searchQuery tersebut menjadi parameter untuk melakukan fetching data ke dalam Rest API.

await Api.get(`/api/admin/categories?q=${searchQuery}`, {

	//...

}

Kemudian kita buat function yang bernama searchHanlder, dimana fungsi ini akan dijalankan ketika form pencarian di dalam JSX di submit.

<form onSubmit={searchHandlder} className="form-group">

	//...

</form>

Ketika form di atas disubmit, maka akan menjalankan function searchHanlder.

//function "searchHandler"
const searchHandlder = (e) => {
  e.preventDefault();

  //call function "fetchDataPost" with params
  fetchData(search)
}

Di dalam function searchHandler kita memanggil function fetchData dengan memberikan parameter state search dan state tersebut akan berisi kata kunci dari input form.

Sekarang, jika kita coba melakukan proses pencarian, maka kurang lebih hasilnya seperti berikut ini :

(https://cdn.jsdelivr.net/gh/maulayyacyber/assets-images-ebooks/wisata-gis-react-laravel-10/Categories - Administrator Travel GIS-categories-search.gif)

Langkah 3 - Membuat Fitur Pagination

Sekarang kita lanjutkan untuk membuat fitur pagination untuk membatasi data yang ditampilkan per-halaman. Dimana kita akan memanfaatkan component yang sudah kita buat sebelumnya.

Silahkan buka file pages/admin/categories/Index.jsx, kemudian ubah semua kode-nya menjadi seperti berikut ini :

//import react  
import React, { useState, useEffect } from "react";

//import layout admin
import LayoutAdmin from "../../../layouts/Admin";

//import BASE URL API
import Api from "../../../api";

//import js cookie
import Cookies from "js-cookie";

//import Link from react router dom
import { Link } from "react-router-dom";

//import pagination component
import PaginationComponent from "../../../components/utilities/Pagination";

function CategoriesIndex() {

	//title page
    document.title = "Categories - Administrator Travel GIS";

    //state posts
    const [categories, setCategories] = useState([]);

    //state currentPage
    const [currentPage, setCurrentPage] = useState(1);

    //state perPage
    const [perPage, setPerPage] = useState(0);

    //state total
    const [total, setTotal] = useState(0);

    //state search
    const [search, setSearch] = useState("");

    //token
    const token = Cookies.get("token");

    //function "fetchData"
    const fetchData = async (pageNumber, searchData) => {

        //define variable "page"
        const page = pageNumber ? pageNumber : currentPage;

        //define variable "searchQuery"
        const searchQuery = searchData ? searchData : search;

        //fetching data from Rest API
        await Api.get(`/api/admin/categories?q=${searchQuery}&page=${page}`, {
            headers: {
                //header Bearer + Token
                Authorization: `Bearer ${token}`,
            }
        })
        .then(response => {
            //set data response to state "categories"
            setCategories(response.data.data.data);

            //set currentPage
            setCurrentPage(response.data.data.current_page);

            //set perPage
            setPerPage(response.data.data.per_page);

            //total
            setTotal(response.data.data.total);
        });
    };

    //hook
    useEffect(() => {
        //call function "fetchData"
        fetchData();

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    //function "searchHandler"
    const searchHandlder = (e) => {
        e.preventDefault();

        //call function "fetchDataPost" with params
        fetchData(1, search)
    }

    return(
        <React.Fragment>
            <LayoutAdmin>
                <div className="row mt-4">
                    <div className="col-12">
                        <div className="card border-0 rounded shadow-sm border-top-success">
                            <div className="card-header">
                                <span className="font-weight-bold"><i className="fa fa-folder"></i> CATEGORIES</span>
                            </div>
                            <div className="card-body">
                                <form onSubmit={searchHandlder} className="form-group">
                                    <div className="input-group mb-3">
                                        <Link to="/admin/categories/create" className="btn btn-md btn-success"><i className="fa fa-plus-circle"></i> ADD NEW</Link>
                                        <input type="text" className="form-control" value={search} onChange={(e) => setSearch(e.target.value)} placeholder="search by category name" />
                                        <button type="submit" className="btn btn-md btn-success"><i className="fa fa-search"></i> SEARCH</button>
                                    </div>
                                </form>
                                <div className="table-responsive">
                                    <table className="table table-bordered table-striped table-hovered">
                                        <thead>
                                        <tr>
                                            <th scope="col">No.</th>
                                            <th scope="col">Image</th>
                                            <th scope="col">Category Name</th>
                                            <th scope="col">Actions</th>
                                        </tr>
                                        </thead>
                                        <tbody>
                                        {categories.map((category, index) => (
                                            <tr key={index}>
                                                <td className="text-center">{++index + (currentPage-1) * perPage}</td>
                                                <td className="text-center">
                                                    <img src={category.image} alt="" width="50" />
                                                </td>
                                                <td>{category.name}</td>
                                                <td className="text-center"></td>
                                            </tr>
                                        ))}
                                        </tbody>
                                    </table>
                                    <PaginationComponent 
                                        currentPage={currentPage} 
                                        perPage={perPage} 
                                        total={total} 
                                        onChange={(pageNumber) => fetchData(pageNumber)}
                                        position="end"
                                    />
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </LayoutAdmin>
        </React.Fragment>
    )

}

export default CategoriesIndex

Dari perubahan kode di atas, pertama kita import component Pagination yang sudah pernah kita buat sebelumnya.

//import pagination component
import PaginationComponent from "../../../components/utilities/Pagination";

Setelah itu, kita melakukan perubahan di dalam function fetchData, dimana kita tambahkan menjadi 2 parameter. Parameter pertama untuk pagination dan parameter kedua untuk pencarian.

//function "fetchData"
const fetchData = async (pageNumber, searchData) => {

	//...

}

Di dalam function fetchData kita juga menambahkan 1 variable baru dengan nama page, variable berisi nilai dari parameter atau state currentPage.

Dan variable page tersebut kita gunakan sebagai parameter di dalam proses fetching ke Rest API.

//fetching data from Rest API
await Api.get(`/api/admin/categories?q=${searchQuery}&page=${page}`, {

	//...
	
}

Dan di dalam function searchHandler kita juga melakukan sedikit perubahan, dimana saat memanggil function fetchData kita berikan 2 parameter, yaitu untuk pagination dengan default 1 dan state search.

//call function "fetchDataPost" with params
fetchData(1, search)

kemudian untuk menampilkan pagination, kita cukup seperti berikut ini :

<PaginationComponent 
   currentPage={currentPage} 
   perPage={perPage} 
   total={total} 
   onChange={(pageNumber) => fetchData(pageNumber)}
   position="end"
/>

Di atas, kita mengirimkan beberapa data sebagai props, yaitu currentPage, perPage, total, onChange dan position.

Sekarang, jika kita reload/refresh halaman-nya, maka kita akan mendapatkan hasil kurang lebih seperti berikut ini :

Langkah 4 - Membuat Proses Hapus Data

Sekarang kita lanjutkan untuk membuat fitur hapus data category. Dimana sebelum data dihapus kita akan menampilkan sebuah jendela konfirmasi, apakah yakin data akan benar-benar dihapus atau tidak.

Dan kita akan menggunakan package tambahan untuk menampilkan jendela konfirmasi tersebut, yaitu React Confirm Alert.

Silahkan buka file pages/admin/categories/Index.jsx, kemudian ubah semua kode-nya menjadi seperti berikut ini :

//import react  
import React, { useState, useEffect } from "react";

//import layout admin
import LayoutAdmin from "../../../layouts/Admin";

//import BASE URL API
import Api from "../../../api";

//import js cookie
import Cookies from "js-cookie";

//import Link from react router dom
import { Link } from "react-router-dom";

//import pagination component
import PaginationComponent from "../../../components/utilities/Pagination";

//import toats
import toast from "react-hot-toast";

//import react-confirm-alert
import { confirmAlert } from 'react-confirm-alert';

//import CSS react-confirm-alert
import 'react-confirm-alert/src/react-confirm-alert.css'; // Import css

function CategoriesIndex() {

    //title page
    document.title = "Categories - Administrator Travel GIS";

    //state posts
    const [categories, setCategories] = useState([]);

    //state currentPage
    const [currentPage, setCurrentPage] = useState(1);

    //state perPage
    const [perPage, setPerPage] = useState(0);

    //state total
    const [total, setTotal] = useState(0);

    //state search
    const [search, setSearch] = useState("");

    //token
    const token = Cookies.get("token");

    //function "fetchData"
    const fetchData = async (pageNumber, searchData) => {

        //define variable "page"
        const page = pageNumber ? pageNumber : currentPage;

        //define variable "searchQuery"
        const searchQuery = searchData ? searchData : search;

        //fetching data from Rest API
        await Api.get(`/api/admin/categories?q=${searchQuery}&page=${page}`, {
            headers: {
                //header Bearer + Token
                Authorization: `Bearer ${token}`,
            }
        })
        .then(response => {
            //set data response to state "categories"
            setCategories(response.data.data.data);

            //set currentPage
            setCurrentPage(response.data.data.current_page);

            //set perPage
            setPerPage(response.data.data.per_page);

            //total
            setTotal(response.data.data.total);
        });
    };

    //hook
    useEffect(() => {
        //call function "fetchData"
        fetchData();

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    //function "searchHandler"
    const searchHandlder = (e) => {
        e.preventDefault();

        //call function "fetchDataPost" with params
        fetchData(1, search)
    }

    //function "deleteCategory"
    const deleteCategory = (id) => {

        //show confirm alert
        confirmAlert({
            title: 'Are You Sure ?',
            message: 'want to delete this data ?',
            buttons: [{
                    label: 'YES',
                    onClick: async () => {
                        await Api.delete(`/api/admin/categories/${id}`, {
                                headers: {
                                    //header Bearer + Token
                                    Authorization: `Bearer ${token}`,
                                }
                            })
                            .then(() => {

                                //show toast
                                toast.success("Data Deleted Successfully!", {
                                    duration: 4000,
                                    position: "top-right",
                                    style: {
                                        borderRadius: '10px',
                                        background: '#333',
                                        color: '#fff',
                                    },
                                });

                                //call function "fetchData"
                                fetchData();
                            })
                    }
                },
                {
                    label: 'NO',
                    onClick: () => {}
                }
            ]
        });
    }

    return(
        <React.Fragment>
            <LayoutAdmin>
                <div className="row mt-4">
                    <div className="col-12">
                        <div className="card border-0 rounded shadow-sm border-top-success">
                            <div className="card-header">
                                <span className="font-weight-bold"><i className="fa fa-folder"></i> CATEGORIES</span>
                            </div>
                            <div className="card-body">
                                <form onSubmit={searchHandlder} className="form-group">
                                    <div className="input-group mb-3">
                                        <Link to="/admin/categories/create" className="btn btn-md btn-success"><i className="fa fa-plus-circle"></i> ADD NEW</Link>
                                        <input type="text" className="form-control" value={search} onChange={(e) => setSearch(e.target.value)} placeholder="search by category name" />
                                        <button type="submit" className="btn btn-md btn-success"><i className="fa fa-search"></i> SEARCH</button>
                                    </div>
                                </form>
                                <div className="table-responsive">
                                    <table className="table table-bordered table-striped table-hovered">
                                        <thead>
                                        <tr>
                                            <th scope="col">No.</th>
                                            <th scope="col">Image</th>
                                            <th scope="col">Category Name</th>
                                            <th scope="col">Actions</th>
                                        </tr>
                                        </thead>
                                        <tbody>
                                        {categories.map((category, index) => (
                                            <tr key={index}>
                                                <td className="text-center">{++index + (currentPage-1) * perPage}</td>
                                                <td className="text-center">
                                                    <img src={category.image} alt="" width="50" />
                                                </td>
                                                <td>{category.name}</td>
                                                <td className="text-center">
                                                	<button onClick={() => deleteCategory(category.id)} className="btn btn-sm btn-danger"><i className="fa fa-trash"></i></button>
                                                </td>
                                            </tr>
                                        ))}
                                        </tbody>
                                    </table>
                                    <PaginationComponent 
                                        currentPage={currentPage} 
                                        perPage={perPage} 
                                        total={total} 
                                        onChange={(pageNumber) => fetchData(pageNumber)}
                                        position="end"
                                    />
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </LayoutAdmin>
        </React.Fragment>
    )

}

export default CategoriesIndex

Dari perubahan kode yang kita lakukan di atas, pertama kita import React Hot Toast, karena kita akan tampilkan sebuah notifikasi setelah proses delete berhasil dilakukan.

//import toats
import toast from "react-hot-toast";

Setelah itu, kita import component dan CSS dari React Confirm Alert.

//import react-confirm-alert
import { confirmAlert } from 'react-confirm-alert';

//import CSS react-confirm-alert
import 'react-confirm-alert/src/react-confirm-alert.css'; // Import css

Jika kita perhatikan, di dalam JSX, kita menambahkan 1 button baru untuk proses delete data category.

<button onClick={() => deleteCategory(category.id)} className="btn btn-sm btn-danger"><i className="fa fa-trash"></i></button>

Button di atas kita berikan event onClick yang mengarah ke dalam function yang bernama deleteCategory dan di dalam function tersebut kita berikan parameter berupa ID dari category.

Jika button di atas diklik, maka akan menjalankan function yang bernama deleteCategory. Kurang lebih seperti berikut ini :

//function "deleteCategory"
const deleteCategory = (id) => {

	//...

}

Di dalam function di atas, pertama kita membuat jendela konfirmasi menggunakan React Confirm Alert.

//show confirm alert
confirmAlert({

	//...

});

Di dalam confirm alert, kita memberikan 2 button untuk opsinya, yaitu YES dan NO.

Jika button YES di klik, maka akan menjalankan sebuah HTTP request ke endpoint /api/admin/categories/:id dengan method DELETE.

await Api.delete(`/api/admin/categories/${id}`, {
    headers: {
        //header Bearer + Token
        Authorization: `Bearer ${token}`,
    }
})

Dan jika proses delete berhasil dilakukan di dalam server, maka kita akan tampilkan notifikasi menggunakan React Hot Toast.

//show toast
toast.success("Data Deleted Successfully!", {
    duration: 4000,
    position: "top-right",
    style: {
        borderRadius: '10px',
        background: '#333',
        color: '#fff',
    },
});

Kemudian kita lakukan fetching lagi dengan cara memanggil function fetchData. Dengan tujuan data diperbarui setelah proses delete.

//call function "fetchData"
fetchData();

Dan jika button NO diklik, kita cukup memberikan sebuah function kosong di dalamnya.

onClick: () => {}

Sekarang jika kita coba reload/refresh halaman categories, maka kita akan mendapatkan 1 button baru untuk delete data. Kurang lebih seperti berikut ini :

Jika kita klik button delete tersebut, maka akan menampilkan jendela konfirmasi kurang lebih seperti berikut ini :

Konfigurasi Private Route Category Create


Pada materi kali ini kita akan belajar untuk membuat konfigurasi private route untuk halaman category create atau tambah data category. Sebelum itu, kita akan membuat view/component-nya terlebih dahulu.

Langkah 1 - Membuat View/Component Category Create

Silahkan buat file baru dengan nama Create.jsx di dalam folder pages/admin/categories/. Setelah itu silahkan masukkan kode berikut ini di dalamnya.

//import react  
import React from "react";

//import layout admin
import LayoutAdmin from "../../../layouts/Admin";

function CategoryCreate() {

    return(
        <React.Fragment>
            <LayoutAdmin>
                <div className="row mt-4">
                    <div className="col-12">
                        <div className="card border-0 rounded shadow-sm border-top-success">
                            <div className="card-header">
                                <span className="font-weight-bold"><i className="fa fa-folder"></i> ADD NEW CATEGORY</span>
                            </div>
                        </div>
                    </div>
                </div>
            </LayoutAdmin>
        </React.Fragment>
    )

}

export default CategoryCreate

Dari penambahan kode di atas, pertama kita import React dari react.

//import react  
import React from "react";

Setelah itu, kita juga import LayoutAdmin, karena view/component ini akan extends dari layout tersebut.

//import layout admin
import LayoutAdmin from "../../../layouts/Admin";

Dan di dalam JSX, kita memberikan sample kode untuk menampilkan halaman. Nantinya akan kita ubah lagi sesuai dengan kebutuhan.

Langkah 2 - Konfigurasi Private Route Category Create

Setelah berhasil membuat view/component untuk halaman category create, sekarang kita lanjutkan untuk melakukan konfigurasi private route-nya.

Silahkan buka file src/routes/routes.jsx, kemudian ubah kode-nya menjadi seperti berikut ini :

//import react router dom
import { Routes, Route } from "react-router-dom";

//=======================================================================
//ADMIN
//=======================================================================

//import view Login
import Login from '../pages/admin/Login.jsx';

//import component private routes
import PrivateRoute from "./PrivateRoutes";

//import view admin Dashboard
import Dashboard from '../pages/admin/dashboard/Index.jsx';

//import view admin categories Index
import CategoriesIndex from '../pages/admin/categories/Index.jsx';

//import view admin category Create
import CategoryCreate from '../pages/admin/categories/Create.jsx';

function RoutesIndex() {
    return (
        <Routes>

            {/* route "/admin/login" */}
            <Route path="/admin/login" element={<Login />} />

            {/* private route "/admin/dashboard" */}
            <Route
                path="/admin/dashboard"
                element={
                        <PrivateRoute>
                            <Dashboard />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories" */}
            <Route
                path="/admin/categories"
                element={
                        <PrivateRoute>
                            <CategoriesIndex />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories/create" */}
            <Route
                path="/admin/categories/create"
                element={
                        <PrivateRoute>
                            <CategoryCreate />
                        </PrivateRoute>
                }
            />

        </Routes>
    )
}

export default RoutesIndex

Dar perubahan kode di atas, pertama kita import view/component CategoryCreate.

//import view admin category Create
import CategoryCreate from '../pages/admin/categories/Create.jsx';

Setelah itu, kita buat konfigurasi private route dengan URL admin/categories/create yang isinya akan melakukan load view/component CategoryCreate.

{/* private route "/admin/categories/create" */}
<Route
    path="/admin/categories/create"
    element={
    <PrivateRoute>
        <CategoryCreate />
    </PrivateRoute>
    }
/>

Sekarang, jika kita klik button ADD NEW yang ada di halaman categories index atau ke URL berikut ini http://localhost:5173/admin/categories/create. Maka kita akan mendapatkan hasil seperti berikut ini :

Membuat Proses Create Data Category


Pada materi sebelumnya kita semua telah belajar bagaimana cara menampilkan data categories melalui Rest API di dalam React.js, kita juga telah belajar menambahkan beberapa fitur seperti pencarian, pagination dan hapus data.

Pada kesempatan kali ini kita akan belajar bagaimana cara melakukan proses insert atau memasukkan data category baru ke dalam database.

Langkah 1 - Edit View/Component Category Create

Karena sudah membuat file view/component untuk halaman category create sebelumnya, maka kita tinggal melakukan penyesuaian saja. Silahkan buka file pages/admin/categories/Create.jsx, kemudian ubah semua kode-nya menjadi seperti berikut ini :

//import hook useState from react
import React, { useState } from "react";

//import layout
import LayoutAdmin from "../../../layouts/Admin";

//import BASE URL API
import Api from "../../../api";

//import hook navigate dari react router dom
import { useNavigate } from "react-router-dom";

//import js cookie
import Cookies from "js-cookie";

//import toats
import toast from "react-hot-toast";

function CategoryCreate() {

    //title page
    document.title = "Add New Category - Administrator Travel GIS";

    //state
    const [name, setName] = useState("");
    const [image, setImage] = useState("");

    //state validation
    const [validation, setValidation] = useState({});

    //token
    const token = Cookies.get("token");

    //navigate
    const navigate = useNavigate();

    //function "handleFileChange"
    const handleFileChange = (e) => {

        //define variable for get value image data
        const imageData = e.target.files[0]

        //check validation file
        if (!imageData.type.match('image.*')) {

            //set state "image" to null
            setImage('');

            //show toast
            toast.error("Format File not Supported!", {
                duration: 4000,
                position: "top-right",
                style: {
                    borderRadius: '10px',
                    background: '#333',
                    color: '#fff',
                },
            });

            return
        }

        //assign file to state "image"
        setImage(imageData);
    }

    //function "storeCategory"
    const storeCategory = async (e) => {
        e.preventDefault();

        //define formData
        const formData = new FormData();

        //append data to "formData"
        formData.append('image', image);
        formData.append('name', name);

        await Api.post('/api/admin/categories', formData, {

                //header
                headers: {
                    //header Bearer + Token
                    'Authorization': `Bearer ${token}`,
                    'content-type': 'multipart/form-data'
                }

            }).then(() => {

                //show toast
                toast.success("Data Saved Successfully!", {
                    duration: 4000,
                    position: "top-right",
                    style: {
                        borderRadius: '10px',
                        background: '#333',
                        color: '#fff',
                    },
                });

                //redirect dashboard page
                navigate("/admin/categories");

            })
            .catch((error) => {

                //set state "validation"
                setValidation(error.response.data);
            })

    }

    return (
        <React.Fragment>
            <LayoutAdmin>
                <div className="row mt-4">
                    <div className="col-12">
                        <div className="card border-0 rounded shadow-sm border-top-success">
                            <div className="card-header">
                                <span className="font-weight-bold"><i className="fa fa-folder"></i> ADD NEW CATEGORY</span>
                            </div>
                            <div className="card-body">
                                <form onSubmit={storeCategory}>
                                    <div className="mb-3">
                                        <label className="form-label fw-bold">Image</label>
                                        <input type="file" className="form-control" onChange={handleFileChange}/>
                                    </div>
                                    {validation.image && (
                                        <div className="alert alert-danger">
                                            {validation.image[0]}
                                        </div>
                                    )}
                                    <div className="mb-3">
                                        <label className="form-label fw-bold">Category Name</label>
                                        <input type="text" className="form-control" value={name} onChange={(e) => setName(e.target.value)} placeholder="Enter Category Name"/>
                                    </div>
                                    {validation.name && (
                                        <div className="alert alert-danger">
                                            {validation.name[0]}
                                        </div>
                                    )}
                                    <div>
                                        <button type="submit" className="btn btn-md btn-success me-2"><i className="fa fa-save"></i> SAVE</button>
                                        <button type="reset" className="btn btn-md btn-warning"><i className="fa fa-redo"></i> RESET</button>
                                    </div>
                                </form>
                            </div>
                        </div>
                    </div>
                </div>
            </LayoutAdmin>
        </React.Fragment>
    );
}

export default CategoryCreate;

Dari perubahan yang kita lakukan di atas, pertama kita import hook dari react, yaitu useState dan useEffect.

//import hook useState from react
import React, { useState } from "react";

Setelah itu, kita melakukan import global endpoint API, karena kita akan melakukan fetching ke dalam server menggunakan Rest API.

//import BASE URL API
import Api from "../../../api";

Kemudian kita import hook useNavigate dari React Router DOM. Ini akan kita gunakan untuk redirect setelah berhasil melakukan proses insert data.

//import hook navigate dari react router dom
import { useNavigate } from "react-router-dom";

Dan kita import package Js Cookie. Karena kita akan gunakan untuk mendapatkan token dari cookies di browser.

//import js cookie
import Cookies from "js-cookie";

Saat proses insert data berhasil kita akan menampilkan sebuah notifikasi, oleh sebab itu kita harus import package React Hot Toast.

//import toats
import toast from "react-hot-toast";

Dan di dalam function component CategoryCreate, pertama kita atur untuk title dari halaman ini.

//title page
document.title = "Add New Category - Administrator Travel GIS";

Setelah itu, kita lanjutkan untuk membuat 2 state baru, yaitu name dan image. State tersebut akan kita gunakan untuk menyimpan data dari input form.

//state
const [name, setName] = useState("");
const [image, setImage] = useState("");

Kemudian kita buat 1 state lagi dengan nama validation. State tersebut akan kita gunakan untuk menyimpan response error validasi dari Rest API.

//state validation
const [validation, setValidation] = useState({});

Dan kita buat variable dengan nama token, dimana isinya adalah token yang berasal dari cookies di browser.

//token
const token = Cookies.get("token");

Untuk mempermudah menggunakan hook useNavigate, maka kita akan letakkan di dalam sebuah variable.

//navigate
const navigate = useNavigate();

Kemudian kita membuat sebuah function yang bernama handleFileChange. Fungsi tersebut akan dijalankan ketika kita memilih sebuah file di dalam input.

<input type="file" className="form-control" onChange={handleFileChange}/>

Di dalam input di atas, kita berikan event onChange yang mengarah ke dalam function yang bernama handleFileChange.

//function "handleFileChange"
const handleFileChange = (e) => {

	//...

}

Fungsi di atas digunakan untuk melakukan validasi terhadap file yang akan diupload. Yaitu apakah file tersebut berupa gambar atau tidak.

//define variable for get value image data
const imageData = e.target.files[0]

//check validation file
if (!imageData.type.match('image.*')) {

	//...

}

Jika file yang akan di upload tidak sesuai dengan format extensi yang sudah ditentukan, maka akan menampilkan sebuah notifikasi error yang berisi informasi format file tidak didukung.

//show toast
toast.error("Format File not Supported!", {
    duration: 4000,
    position: "top-right",
    style: {
        borderRadius: '10px',
        background: '#333',
        color: '#fff',
    },
});

Jika format file yang akan di upload sudah sesuai, maka kita akan assign ke dalam state image.

//assign file to state "image"
setImage(imageData);

Kemudian kita buat function lagi dengan nama storeCategory. Fungsi tersebut akan dijalankan ketika form di dalam JSX disubmit.

<form onSubmit={storeCategory}>

	//...

</form>
//function "storeCategory"
const storeCategory = async (e) => {

	//...

}

Di dalam function storeCategory, pertama-tama kita melakukan inisialisasi FormData. Tujuannya untuk mempermudah kita dalam mengirimkan data ke dalam server.

//define formData
const formData = new FormData();

Setelah itu, kita buat append dari formData, yang berisi state name dan image.

//append data to "formData"
formData.append('image', image);
formData.append('name', name);

INFORMASI : pada append formData, kita mempunyai 2 parameter. Parameter pertama berupa key dan parameter kedua berupa data value.

formData.append('key', 'value');

Setelah data berhasil di masukkan di dalam formData, langkah selanjutnya adalah mengirimkannya ke dalam server menggunakan Rest API.

await Api.post('/api/admin/categories', formData, {

    //header
    headers: {
        //header Bearer + Token
        'Authorization': `Bearer ${token}`,
        'content-type': 'multipart/form-data'
    }

})

Di atas, kita mengirimkan data ke dalam endpoint /api/admin/categories dengan method POST. Dan parameter data-nya adalah formData.

Jika proses insert data berhasil dilakukan, maka kita akan menampilkan sebuah notifikasi menggunakan React Hot Toast yang berisi informasi data berhasil disimpan.

//show toast
toast.success("Data Saved Successfully!", {
    duration: 4000,
    position: "top-right",
    style: {
        borderRadius: '10px',
        background: '#333',
        color: '#fff',
    },
});

Setelah itu, kita redirect atau arahkan ke dalam URL /admin/categories.

Tapi, jika proses insert data gagal dilakukan, maka akan melakukan assign error validasi ke dalam state validation.

//set state "validation"
setValidation(error.response.data);

Langkag 2 - Uji Coba Proses Insert Data

Sekarang silahkan reload/refresh halaman category create. Jika berhasil maka kurang lebih tampilannya seperti berikut ini :

Silahkan klik button SAVE tanpa mengisi data apapun, maka kita akan mendapatkan error validasi dari Rest API. Kurang lebih seperti berikut ini :

Sekarang kita coba memasukkan data gambar dan nama category dan klik SAVE. Jika berhasil maka kita akan di arahkan ke halaman categories index dengan data yang baru saja ditambahkan.

Konfigurasi Private Route Category Edit


Setelah sebelumnya kita telah belajar bagaiaman cara membuat konfigurasi private route dan proses insert data category, maka sekarang kita akan lanjutkan untuk melakukan konfigurasi private route kemudian proses edit dan update data category.

Langkah 1 - Membuat View/Component Category Edit

Pertama-tama kita akan membuat view/component-nya terlebih dahulu untuk halaman edit category. Silahkan buat file baru dengan nama Edit.jsx di dalam folder src/pages/admin/categories. Kemudian masukkan kode berikut ini di dalamnya.

//import react  
import React from "react";

//import layout admin
import LayoutAdmin from "../../../layouts/Admin";

function CategoryEdit() {

    return(
        <React.Fragment>
            <LayoutAdmin>
                <div className="row mt-4">
                    <div className="col-12">
                        <div className="card border-0 rounded shadow-sm border-top-success">
                            <div className="card-header">
                                <span className="font-weight-bold"><i className="fa fa-folder"></i> EDIT CATEGORY</span>
                            </div>
                        </div>
                    </div>
                </div>
            </LayoutAdmin>
        </React.Fragment>
    )

}

export default CategoryEdit

Di atas kita menambahkan sample kode untuk halaman edit category. Di materi selanjutnya akan kita ubah lagi sesuai dengan kebutuhan.

Langkah 2 - Konfigurasi Private Route Category Edit

Setelah berhasil membuat view/component, maka sekarang kita lanjutkan untuk membuat konfigurasi private route-nya. Silahkan buka file src/routes/routes.jsx, kemudia ubah semua kode-nya menjadi seperti berikut ini :

//import react router dom
import { Routes, Route } from "react-router-dom";

//=======================================================================
//ADMIN
//=======================================================================

//import view Login
import Login from '../pages/admin/Login.jsx';

//import component private routes
import PrivateRoute from "./PrivateRoutes";

//import view admin Dashboard
import Dashboard from '../pages/admin/dashboard/Index.jsx';

//import view admin categories Index
import CategoriesIndex from '../pages/admin/categories/Index.jsx';

//import view admin category Create
import CategoryCreate from '../pages/admin/categories/Create.jsx';

//import view admin category Edit
import CategoryEdit from '../pages/admin/categories/Edit.jsx';

function RoutesIndex() {
    return (
        <Routes>

            {/* route "/admin/login" */}
            <Route path="/admin/login" element={<Login />} />

            {/* private route "/admin/dashboard" */}
            <Route
                path="/admin/dashboard"
                element={
                        <PrivateRoute>
                            <Dashboard />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories" */}
            <Route
                path="/admin/categories"
                element={
                        <PrivateRoute>
                            <CategoriesIndex />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories/create" */}
            <Route
                path="/admin/categories/create"
                element={
                        <PrivateRoute>
                            <CategoryCreate />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories/edit/:id" */}
            <Route
                path="/admin/categories/edit/:id"
                element={
                        <PrivateRoute>
                            <CategoryEdit />
                        </PrivateRoute>
                }
            />

        </Routes>
    )
}

export default RoutesIndex

Dari perubahan kode di atas, pertama kita import view/component category edit yang sudah kita buat sebelumnya.

//import view admin category Edit
import CategoryEdit from '../pages/admin/categories/Edit.jsx';

Setelah itu, kita buat konfigurasi private route untuk category edit. Kurang lebih seperti berikut ini :

{/* private route "/admin/categories/edit/:id" */}
<Route
    path="/admin/categories/edit/:id"
    element={
    <PrivateRoute>
        <CategoryEdit />
    </PrivateRoute>
    }
/>

Karena akan memiliki segment URL yang dinamis, maka kita atur di dalam path kurang lebih seperti berikut ini :

/admin/categories/edit/:id

Dimana nilai :id, akan bersifat dinamis dan berubah-ubah.

Konfigurasi Private Route Category Edit


Setelah sebelumnya kita telah belajar bagaiaman cara membuat konfigurasi private route dan proses insert data category, maka sekarang kita akan lanjutkan untuk melakukan konfigurasi private route kemudian proses edit dan update data category.

Langkah 1 - Membuat View/Component Category Edit

Pertama-tama kita akan membuat view/component-nya terlebih dahulu untuk halaman edit category. Silahkan buat file baru dengan nama Edit.jsx di dalam folder src/pages/admin/categories. Kemudian masukkan kode berikut ini di dalamnya.

//import react  
import React from "react";

//import layout admin
import LayoutAdmin from "../../../layouts/Admin";

function CategoryEdit() {

    return(
        <React.Fragment>
            <LayoutAdmin>
                <div className="row mt-4">
                    <div className="col-12">
                        <div className="card border-0 rounded shadow-sm border-top-success">
                            <div className="card-header">
                                <span className="font-weight-bold"><i className="fa fa-folder"></i> EDIT CATEGORY</span>
                            </div>
                        </div>
                    </div>
                </div>
            </LayoutAdmin>
        </React.Fragment>
    )

}

export default CategoryEdit

Di atas kita menambahkan sample kode untuk halaman edit category. Di materi selanjutnya akan kita ubah lagi sesuai dengan kebutuhan.

Langkah 2 - Konfigurasi Private Route Category Edit

Setelah berhasil membuat view/component, maka sekarang kita lanjutkan untuk membuat konfigurasi private route-nya. Silahkan buka file src/routes/routes.jsx, kemudia ubah semua kode-nya menjadi seperti berikut ini :

//import react router dom
import { Routes, Route } from "react-router-dom";

//=======================================================================
//ADMIN
//=======================================================================

//import view Login
import Login from '../pages/admin/Login.jsx';

//import component private routes
import PrivateRoute from "./PrivateRoutes";

//import view admin Dashboard
import Dashboard from '../pages/admin/dashboard/Index.jsx';

//import view admin categories Index
import CategoriesIndex from '../pages/admin/categories/Index.jsx';

//import view admin category Create
import CategoryCreate from '../pages/admin/categories/Create.jsx';

//import view admin category Edit
import CategoryEdit from '../pages/admin/categories/Edit.jsx';

function RoutesIndex() {
    return (
        <Routes>

            {/* route "/admin/login" */}
            <Route path="/admin/login" element={<Login />} />

            {/* private route "/admin/dashboard" */}
            <Route
                path="/admin/dashboard"
                element={
                        <PrivateRoute>
                            <Dashboard />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories" */}
            <Route
                path="/admin/categories"
                element={
                        <PrivateRoute>
                            <CategoriesIndex />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories/create" */}
            <Route
                path="/admin/categories/create"
                element={
                        <PrivateRoute>
                            <CategoryCreate />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories/edit/:id" */}
            <Route
                path="/admin/categories/edit/:id"
                element={
                        <PrivateRoute>
                            <CategoryEdit />
                        </PrivateRoute>
                }
            />

        </Routes>
    )
}

export default RoutesIndex

Dari perubahan kode di atas, pertama kita import view/component category edit yang sudah kita buat sebelumnya.

//import view admin category Edit
import CategoryEdit from '../pages/admin/categories/Edit.jsx';

Setelah itu, kita buat konfigurasi private route untuk category edit. Kurang lebih seperti berikut ini :

{/* private route "/admin/categories/edit/:id" */}
<Route
    path="/admin/categories/edit/:id"
    element={
    <PrivateRoute>
        <CategoryEdit />
    </PrivateRoute>
    }
/>

Karena akan memiliki segment URL yang dinamis, maka kita atur di dalam path kurang lebih seperti berikut ini :

/admin/categories/edit/:id

Dimana nilai :id, akan bersifat dinamis dan berubah-ubah.

Membuat Proses Edit Data Category


Pada materi kali ini kita akan belajar membuat proses edit dan update data ke dalam database menggunakan React.js dan Rest API.

Konsepnya nanti kita akan menampilkan datanya terlebih dahulu ke dalam form edit, setelah data di tampilkan kita bisa melakukan perubahan data tersebut di dalam form sesuai dengan yang diinginkan dan setelah itu kita bisa melakukan proses update data ke dalam database menggunakan Rest API.

Langkah 1 - Menampilkan Button Edit

Sebelum kita lanjutkan untuk merubah halaman edit category, pertama-tama kita akan tambahkan button edit terlebih dahulu di dalam halaman categories index.

Silahkan buka file src/pages/admin/categories/Index.jsx, kemudian cari kode berikut ini :

<td className="text-center">
    <button onClick={() => deleteCategory(category.id)} className="btn btn-sm btn-danger"><i className="fa fa-trash"></i></button>
</td>

Kemudian ubah kode-nya menjadi seperti berikut ini :

<td className="text-center">
	<Link to={`/admin/categories/edit/${category.id}`} className="btn btn-sm btn-primary me-2"><i className="fa fa-pencil-alt"></i></Link>
    <button onClick={() => deleteCategory(category.id)} className="btn btn-sm btn-danger"><i className="fa fa-trash"></i></button>
</td>

Dari perubahan kode di atas, kita menambahkan sebuah button baru yang berisi link menuju halaman edit data category dengan parameter ID.

Jika kita reload/refresh halaman categories index, maka kita akan mendapatkan button baru untuk edit data. Kurang lebih seperti berikut ini :

Langkah 2 - Edit View/Component Category Edit

Sekarang kita lanjutkan untuk melakukan perubahan kode di dalam file category edit. Silahkan buka file src/pages/admin/categories/Edit.jsx, kemudian ubah kode-nya menjadi seperti berikut ini :

//import hook useState from react
import React, { useState, useEffect } from "react";

//import layout
import LayoutAdmin from "../../../layouts/Admin";

//import BASE URL API
import Api from "../../../api";

//import hook navigate dari react router dom
import { useNavigate, useParams } from "react-router-dom";

//import js cookie
import Cookies from "js-cookie";

//import toats
import toast from "react-hot-toast";

function CategoryEdit() {

	//title page
    document.title = "Edit Category - Administrator Travel GIS";

    //state
    const [name, setName] = useState("");
    const [image, setImage] = useState("");

    //state validation
    const [validation, setValidation] = useState({});

    //token
    const token = Cookies.get("token");

    //naviagte
    const navigate = useNavigate();

    //get ID from parameter URL
    const { id } = useParams();

    //function "getCategoryById"
    const getCategoryById = async () => {

        //get data from server
        const response = await Api.get(`/api/admin/categories/${id}`, {

            //header
            headers: {
                //header Bearer + Token
                Authorization: `Bearer ${token}`,
            }
        });

        //get response data
        const data = await response.data.data

        //assign data to state "name"
        setName(data.name);
    };

    //hook useEffect
    useEffect(() => {

        //panggil function "getCategoryById"
        getCategoryById();

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);


    //function "handleFileChange"
    const handleFileChange = (e) => {

        //define variable for get value image data
        const imageData = e.target.files[0]

        //check validation file
        if (!imageData.type.match('image.*')) {

            //set state "image" to null
            setImage('');

            //show toast
            toast.error("Format File not Supported!", {
                duration: 4000,
                position: "top-right",
                style: {
                    borderRadius: '10px',
                    background: '#333',
                    color: '#fff',
                },
            });

            return
        }

        //assign file to state "image"
        setImage(imageData);
    }

    //function "updateCategory"
    const updateCategory = async (e) => {
        e.preventDefault();

        //define formData
        const formData = new FormData();

        //append data to "formData"
        formData.append('image', image);
        formData.append('name', name);
        formData.append('_method', 'PATCH');

        await Api.post(`/api/admin/categories/${id}`, formData, {

                //header
                headers: {
                    //header Bearer + Token
                    'Authorization': `Bearer ${token}`,
                    'content-type': 'multipart/form-data'
                }

            }).then(() => {

                //show toast
                toast.success("Data Updated Successfully!", {
                    duration: 4000,
                    position: "top-right",
                    style: {
                        borderRadius: '10px',
                        background: '#333',
                        color: '#fff',
                    },
                });

                //redirect dashboard page
                navigate("/admin/categories");

            })
            .catch((error) => {

                //set state "validation"
                setValidation(error.response.data);
            })

    }

    return (
        <React.Fragment>
            <LayoutAdmin>
                <div className="row mt-4">
                    <div className="col-12">
                        <div className="card border-0 rounded shadow-sm border-top-success">
                            <div className="card-header">
                                <span className="font-weight-bold"><i className="fa fa-folder"></i> EDIT CATEGORY</span>
                            </div>
                            <div className="card-body">
                                <form onSubmit={updateCategory}>
                                    <div className="mb-3">
                                        <label className="form-label fw-bold">Image</label>
                                        <input type="file" className="form-control" onChange={handleFileChange}/>
                                    </div>
                                    {validation.image && (
                                        <div className="alert alert-danger">
                                            {validation.image[0]}
                                        </div>
                                    )}
                                    <div className="mb-3">
                                        <label className="form-label fw-bold">Category Name</label>
                                        <input type="text" className="form-control" value={name} onChange={(e) => setName(e.target.value)} placeholder="Enter Category Name"/>
                                    </div>
                                    {validation.name && (
                                        <div className="alert alert-danger">
                                            {validation.name[0]}
                                        </div>
                                    )}
                                    <div>
                                        <button type="submit" className="btn btn-md btn-success me-2"><i className="fa fa-save"></i> UPDATE</button>
                                        <button type="reset" className="btn btn-md btn-warning"><i className="fa fa-redo"></i> RESET</button>
                                    </div>
                                </form>
                            </div>
                        </div>
                    </div>
                </div>
            </LayoutAdmin>
        </React.Fragment>
    );
}

export default CategoryEdit;

Dari perubahan kode di atas, pertama kita import hook dari react, yaitu useState dan useEffect.

//import hook useState from react
import React, { useState, useEffect } from "react";

Karena akan berurusan dengan Rest API, maka kita akan import konfigurasi global endpoint.

//import BASE URL API
import Api from "../../../api";

Kemudian kita juga import hook dari React Router DOM, yaitu useNavigate dan useParams. Untuk hook useNavigate diguankan untuk melakukan navigasi ke dalam sebuah URL. Sedangkan untuk hook useParams digunakan untuk mendapatkan parameter dari URL.

//import hook navigate dari react router dom
import { useNavigate, useParams } from "react-router-dom";

Dan kita import package Js Cookie, karena akan kita gunakan untuk mendapatkan token di dalam cookies browser.

//import js cookie
import Cookies from "js-cookie";

Karena akan menampilkan notifikasi setelah update data, maka kita akan import package React Hot Toast.

//import toats
import toast from "react-hot-toast";

Di dalam function component CategoryEdit, pertama-tama kita atur untuk title dari halaman ini.

//title page
document.title = "Edit Category - Administrator Travel GIS";

Setelah itu, kita definisikan 2 state untuk menyimpan data dari input form, yaitu name dan image.

//state
const [name, setName] = useState("");
const [image, setImage] = useState("");

Kemudian kita buat 1 state lagi dengan nama validation, state tersebut akan kita gunakan untuk menyimpan data error response dari Rest API.

//state validation
const [validation, setValidation] = useState({});

Kemudian kita buat variable dengan nama token yang isinya adalah token yang diambil dari cookies. Dan variable navigate untuk mempermudah kita dalam menggunakan hook useNavigate.

//token
const token = Cookies.get("token");

//navigate
const navigate = useNavigate();

Setelah itu, kita ambil object ID yang ada di dalam hook useParams atau kita lakukan destructuring. Ini akan kita gunakan untuk dijadikan sebagai parameter fetching data ke dalam Rest API.

//get ID from parameter URL
const { id } = useParams();

kemudian kita buat function yang bernama getCategoryById dengan jenis asynchronus.

//function "getCategoryById"
const getCategoryById = async () => {

	//...
	
}

Di dalamnya kita melakukan fetching ke dalam endpoint api/admin/categories/:id dengan method GET.

//get data from server
const response = await Api.get(`/api/admin/categories/${id}`, {

    //header
    headers: {
        //header Bearer + Token
        Authorization: `Bearer ${token}`,
    }
});

Jika proses fetching berhasil, maka kita akan simpan response data-nya ke dalam variable yang bernama data.

//get response data
const data = await response.data.data

Setelah itu, kita assign data tersebut ke dalam state name.

//assign data to state "name"
setName(data.name);

Agar function getCategoryById dapat dijalankan pertama kali saat halaman diload, maka kita perlu memanggilnya di dalam hook useEffect.

//hook useEffect
useEffect(() => {

    //panggil function "getCategoryById"
    getCategoryById();

    // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Kemudian kita membuat sebuah function yang bernama handleFileChange. Fungsi tersebut akan dijalankan ketika kita memilih sebuah file di dalam input.

<input type="file" className="form-control" onChange={handleFileChange}/>

Di dalam input di atas, kita berikan event onChange yang mengarah ke dalam function yang bernama handleFileChange.

//function "handleFileChange"
const handleFileChange = (e) => {

	//...

}

Fungsi di atas digunakan untuk melakukan validasi terhadap file yang akan diupload. Yaitu apakah file tersebut berupa gambar atau tidak.

//define variable for get value image data
const imageData = e.target.files[0]

//check validation file
if (!imageData.type.match('image.*')) {

	//...

}

Jika file yang akan diupload tidak sesuai dengan format extensi yang sudah ditentukan, maka akan menampilkan sebuah notifikasi error yang berisi informasi format file tidak didukung.

//show toast
toast.error("Format File not Supported!", {
    duration: 4000,
    position: "top-right",
    style: {
        borderRadius: '10px',
        background: '#333',
        color: '#fff',
    },
});

Jika format file yang akan diupload sudah sesuai, maka kita akan assign ke dalam state image.

//assign file to state "image"
setImage(imageData);

Kemudian kita buat function yang bernama updateCategory. Fungsi tersebut akan dijalankan ketika form disubmit.

<form onSubmit={updateCategory}>

	//...

</form>
//function "updateCategory"
const updateCategory = async (e) => {

	//...

}

Di dalam function updateCategory, pertama-tama kita melakukan inisialisasi formData. Tujuannya untuk mempermudah dalam pengiriman sebuah data ke dalam server melalui Rest API.

//define formData
const formData = new FormData();

Setelah inisialisasi formData, selanjutnya kita lakukan append data yang ada di dalam state ke dalam formData.

//append data to "formData"
formData.append('image', image);
formData.append('name', name);
formData.append('_method', 'PATCH');

Kemudian kita akan kirimkan formData tersebut ke dalam server.

await Api.post(`/api/admin/categories/${id}`, formData, {

    //header
    headers: {
        //header Bearer + Token
        'Authorization': `Bearer ${token}`,
        'content-type': 'multipart/form-data'
    }

})

Jika proses update data berhasil dilakukan di dalam server, maka kita akan menampilkan notifikasi menggunakan React Hot Toast yang bersi informasi data berhasil diupdate.

//show toast
toast.success("Data Updated Successfully!", {
    duration: 4000,
    position: "top-right",
    style: {
        borderRadius: '10px',
        background: '#333',
        color: '#fff',
    },
});

Setelah itu, kita redirect ke halaman categories index.

//redirect dashboard page
navigate("/admin/categories");

Tapi, jika proses update data gagal dilakukan, maka kita akan assign error response ke dalam state validation.

//set state "validation"
setValidation(error.response.data);

Dan untuk menampilkan error validation di dalam JSX, kita bisa seperti berikut ini :

{validation.image && (
    <div className="alert alert-danger">
       {validation.image[0]}
    </div>
)}

Langkah 3 - Uji Coba Proses Edit dan Update Category

Sekarang silahkan klik button edit di salah satu data category. Jika berhasil maka kita akan menampilkan halaman edit category seperti berikut ini :

Silahkan ubah data-nya sesuai dengan keinginan dan untuk input gambar kita tidak wajib mengisinya. Dan jika berhasil maka kurang lebih seperti berikut ini hasilnya.

Konfigurasi Private Route Places Index


Setelah berhasil membuat CRUD data master categories di dalam React.js, maka sekarang kita akan lanjutkan untuk data places. Sebelum itu pertama-tama kita akan membuat konfigurasi private route-nya terlebih dahulu.

Langkah 1 - Membuat View/Component Places Index

Sekarang kita akan membuat view/component untuk data places index terlebih dahulu. Silahkan buat folder baru dengan nama places di dalam folder src/pages/admin/. Setelah itu silahkan buat file baru dengan nama Index.jsx di dalam folder places tersebut dan masukkan kode berikut ini :

//import react  
import React from "react";

//import layout admin
import LayoutAdmin from "../../../layouts/Admin";

function PlacesIndex() {

    return(
        <React.Fragment>
            <LayoutAdmin>
                <div className="row mt-4">
                    <div className="col-12">
                        <div className="card border-0 rounded shadow-sm border-top-success">
                            <div className="card-header">
                                <span className="font-weight-bold"><i className="fa fa-map-marked-alt"></i> PLACES</span>
                            </div>
                        </div>
                    </div>
                </div>
            </LayoutAdmin>
        </React.Fragment>
    )

}

export default PlacesIndex

Di atas kita hanya memberikan sample kode untuk halaman place index, nantinya kita akan ubah lagi sesuai dengan kebutuhan.

Lagkah 2 - Konfigurasi Private Route Places Index

Setelah berhasil membuat view/component untuk places index, sekarang kita lanjutkan untuk membuat konfigurasi private route-nya. Silahkan buka file src/routes/routes.jsx, kemudian ubah kode-nya menjadi seperti berikut ini :

//import react router dom
import { Routes, Route } from "react-router-dom";

//=======================================================================
//ADMIN
//=======================================================================

//import view Login
import Login from '../pages/admin/Login.jsx';

//import component private routes
import PrivateRoute from "./PrivateRoutes";

//import view admin Dashboard
import Dashboard from '../pages/admin/dashboard/Index.jsx';

//import view admin categories Index
import CategoriesIndex from '../pages/admin/categories/Index.jsx';

//import view admin category Create
import CategoryCreate from '../pages/admin/categories/Create.jsx';

//import view admin category Edit
import CategoryEdit from '../pages/admin/categories/Edit.jsx';

//import view admin places Index
import PlacesIndex from '../pages/admin/places/Index.jsx';

function RoutesIndex() {
    return (
        <Routes>

            {/* route "/admin/login" */}
            <Route path="/admin/login" element={<Login />} />

            {/* private route "/admin/dashboard" */}
            <Route
                path="/admin/dashboard"
                element={
                        <PrivateRoute>
                            <Dashboard />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories" */}
            <Route
                path="/admin/categories"
                element={
                        <PrivateRoute>
                            <CategoriesIndex />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories/create" */}
            <Route
                path="/admin/categories/create"
                element={
                        <PrivateRoute>
                            <CategoryCreate />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories/edit/:id" */}
            <Route
                path="/admin/categories/edit/:id"
                element={
                        <PrivateRoute>
                            <CategoryEdit />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/places" */}
            <Route
                path="/admin/places"
                element={
                        <PrivateRoute>
                            <PlacesIndex />
                        </PrivateRoute>
                }
            />

        </Routes>
    )
}

export default RoutesIndex

Dari perubahan kode di atas, pertama kita import file view/component place index yang sudah kita buat sebelumnya.

//import view admin places Index
import PlacesIndex from '../pages/admin/places/Index.jsx';

Setelah itu, kita buatkan konfigurasi private route-nya kurang lebih seperti berikut ini :

{/* private route "/admin/places" */}
<Route
    path="/admin/places"
    element={
    <PrivateRoute>
        <PlacesIndex />
    </PrivateRoute>
    }
/>

Di atas, untuk path-nya kita atur ke dalam /admin/places dan ketika URL tersebut diakses, maka akan memanggil view/component PlacesIndex.

Sekarang coba buka URL berikut ini http://localhost:5173/admin/places atau bisa klik menu PLACES yang ada di menu sidebar. Jika berhasil maka akan muncul halaman seperti berikut ini :

Menampilkan Data Places


Pada kesempatan kali ini kita semua akan belajar bagaimana cara menampilkan list data places yang ada di dalam database ke dalam React.js menggunakan Rest API. Dan sama seperti materi-materi sebelumnya, yaitu kita akan menambahkan beberapa fitur, diantaranya adalah pagination, pencarian data dan menghapus data.

Langkah 1 - Menampilkan Data Places

Sekarang kita akan belajar untuk menampilkan data places dari server ke dalam React.js menggunakan Rest API dan kita akan melakukan perubahan di dalam file yang sudah pernah kita buat sebelumnya.

Silahkan buka file pages/admin/places/Index.jsx, kemudian ubah semua kode-nya menjadi seperti berikut ini :

//import react
import React, { useState, useEffect } from "react";

//import layout
import LayoutAdmin from "../../../layouts/Admin";

//import BASE URL API
import Api from "../../../api";

//import js cookie
import Cookies from "js-cookie";

function PlacesIndex() {

	//title page
    document.title = "Places - Administrator Travel GIS";

    //state places
    const [places, setPlaces] = useState([]);

    //state currentPage
    const [currentPage, setCurrentPage] = useState(1);

    //state perPage
    const [perPage, setPerPage] = useState(0);

    //state total
    const [total, setTotal] = useState(0);

    //token
    const token = Cookies.get("token");

    //function "fetchData"
    const fetchData = async () => {

        //fetching data from Rest API
        await Api.get('/api/admin/places', {
            headers: {
                //header Bearer + Token
                Authorization: `Bearer ${token}`,
            }
        }).then(response => {
            //set data response to state "places"
            setPlaces(response.data.data.data);

            //set currentPage
            setCurrentPage(response.data.data.current_page);

            //set perPage
            setPerPage(response.data.data.per_page);

            //total
            setTotal(response.data.data.total);
        });
    };

    //hook
    useEffect(() => {
        //call function "fetchData"
        fetchData();

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    return (
        <React.Fragment>
            <LayoutAdmin>
              <div className="row mt-4">
                <div className="col-12">
                  <div className="card border-0 rounded shadow-sm border-top-success">
                    <div className="card-header">
                        <span className="font-weight-bold"><i className="fa fa-map-marked-alt"></i> PLACES</span>
                    </div>
                    <div className="card-body">
                      
                      <div className="table-responsive">
                        <table className="table table-bordered table-striped table-hovered">
                            <thead>
                            <tr>
                                <th scope="col">No.</th>
                                <th scope="col">Title</th>
                                <th scope="col">Category</th>
                                <th scope="col">Actions</th>
                            </tr>
                            </thead>
                            <tbody>
                            {places.map((place, index) => (
                                <tr key={index}>
                                    <td className="text-center">{++index + (currentPage-1) * perPage}</td>
                                    <td>{place.title}</td>
                                    <td>{place.category.name}</td>
                                    <td className="text-center"></td>
                                </tr>
                            ))}
                            </tbody>
                        </table>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </LayoutAdmin>
        </React.Fragment>
    );
}

export default PlacesIndex;

Dari perubahan kode di atas, pertama kita import hook dari react, yaitu useState dan useEffect.

//import react
import React, { useState, useEffect } from "react";

Karena akan melakukan proses fetching ke Rest API, maka kita butuh import global endpoint API.

//import BASE URL API
import Api from "../../../api";

Dan karena API yang akan kita fetch membutuhkan token, maka kita akan import package Js Cookie yang berfungsi untuk mempermudah kita dalam mendapatkan token di dalam cookie browser.

//import js cookie
import Cookies from "js-cookie";

Di dalam function component PlaceIndex, pertama kita atur title dari halaman ini.

//title page
document.title = "Places - Administrator Travel GIS";

Setelah itu, kita buat beberapa state untuk menyimpan data dari response Rest API.

//state places
const [places, setPlaces] = useState([]);

//state currentPage
const [currentPage, setCurrentPage] = useState(1);

//state perPage
const [perPage, setPerPage] = useState(0);

//state total
const [total, setTotal] = useState(0);

Untuk state places akan kita gunakan untuk menyimpan list data places dari response Rest API. Sedangkan state currentPage akan diguankan untuk menyimpan nomor halaman dari pagination yang aktif. Dan state perPage digunakan untuk menentukan jumlah data yang ditampilkan per-halaman. Kemudian yang terakhir state total digunakan untuk menyimpan total data places.

Kemudian kita buat variable dengan nama token yang isinya adalah token yang berada di dalam cookies browser.

//token
const token = Cookies.get("token");

Setelah itu, kita buat function baru dengan nama fetchData dengan jenis asynchronus. Fungsi ini akan kita gunakan untuk melakukan HTTP request ke dalam server menggunakan Rest API.

//function "fetchData"
const fetchData = async () => {

	//...
	
}

Di dalam function tersebut kita melakukan fetching ke dalam endpoint /api/admin/places dengan method GET.

//fetching data from Rest API
await Api.get('/api/admin/places', {
    headers: {
        //header Bearer + Token
        Authorization: `Bearer ${token}`,
    }
})

Jika berhasil, maka kita akan assign response data dari Rest API ke dalam beberapa state.

//set data response to state "places"
setPlaces(response.data.data.data);

//set currentPage
setCurrentPage(response.data.data.current_page);

//set perPage
setPerPage(response.data.data.per_page);

//total
setTotal(response.data.data.total);

Kemudian, agar function fetchData dapat dijalankan ketika halaman diload, maka kita perlu memanggilnya di dalam hook useEffect.

//hook
useEffect(() => {
    //call function "fetchData"
    fetchData();

    // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Dan untuk menampilkan data di dalam JSX, kita bisa menggunakan perulangan dengan map.

{places.map((place, index) => (

	//...

))}

Sekarang jika halaman places index direload/direfresh. Maka kita akan mendapatkan hasil seperti berikut ini :

Langkah 2 - Membuat Fitur Pencarian

Setelah berhasil menampilkan data, sekarang kita akan lanjutkan untuk menambahkan fitur pencarian data. Dan disini kita akan melakukan perubahan dengan menambahkan beberapa kode di dalam file yang sama.

Silahkan buka file src/pages/admin/places/Index.jsx, kemudian ubah semua kode-nya menjadi seperti berikut ini :

//import react
import React, { useState, useEffect } from "react";

//import layout
import LayoutAdmin from "../../../layouts/Admin";

//import BASE URL API
import Api from "../../../api";

//import js cookie
import Cookies from "js-cookie";

//import Link from react router dom
import { Link } from "react-router-dom";

function PlacesIndex() {

    //title page
    document.title = "Places - Administrator Travel GIS";

    //state places
    const [places, setPlaces] = useState([]);

    //state currentPage
    const [currentPage, setCurrentPage] = useState(1);

    //state perPage
    const [perPage, setPerPage] = useState(0);

    //state total
    const [total, setTotal] = useState(0);

    //state search
    const [search, setSearch] = useState("");

    //token
    const token = Cookies.get("token");

    //function "fetchData"
    const fetchData = async (searchData) => {

        //define variable "searchQuery"
        const searchQuery = searchData ? searchData : search;

        //fetching data from Rest API
        await Api.get(`/api/admin/places?q=${searchQuery}`, {
            headers: {
                //header Bearer + Token
                Authorization: `Bearer ${token}`,
            }
        }).then(response => {
            //set data response to state "places"
            setPlaces(response.data.data.data);

            //set currentPage
            setCurrentPage(response.data.data.current_page);

            //set perPage
            setPerPage(response.data.data.per_page);

            //total
            setTotal(response.data.data.total);
        });
    };

    //hook
    useEffect(() => {
        //call function "fetchData"
        fetchData();

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    //function "searchHandler"
    const searchHandlder = (e) => {
        e.preventDefault();

        //call function "fetchDataPost"
        fetchData(search)
    }

    return (
        <React.Fragment>
            <LayoutAdmin>
              <div className="row mt-4">
                <div className="col-12">
                  <div className="card border-0 rounded shadow-sm border-top-success">
                    <div className="card-header">
                        <span className="font-weight-bold"><i className="fa fa-map-marked-alt"></i> PLACES</span>
                    </div>
                    <div className="card-body">

                        <form onSubmit={searchHandlder} className="form-group">
                            <div className="input-group mb-3">
                                <Link to="/admin/places/create" className="btn btn-md btn-success"><i className="fa fa-plus-circle"></i> ADD NEW</Link>
                                <input type="text" className="form-control" value={search} onChange={(e) => setSearch(e.target.value)} placeholder="search by place title" />
                                <button type="submit" className="btn btn-md btn-success"><i className="fa fa-search"></i> SEARCH</button>
                            </div>
                        </form>
                      
                        <div className="table-responsive">
                            <table className="table table-bordered table-striped table-hovered">
                                <thead>
                                <tr>
                                    <th scope="col">No.</th>
                                    <th scope="col">Title</th>
                                    <th scope="col">Category</th>
                                    <th scope="col">Actions</th>
                                </tr>
                                </thead>
                                <tbody>
                                {places.map((place, index) => (
                                    <tr key={index}>
                                        <td className="text-center">{++index + (currentPage-1) * perPage}</td>
                                        <td>{place.title}</td>
                                        <td>{place.category.name}</td>
                                        <td className="text-center"></td>
                                    </tr>
                                ))}
                                </tbody>
                            </table>
                        </div>
                    </div>
                  </div>
                </div>
              </div>
            </LayoutAdmin>
        </React.Fragment>
    );
}

export default PlacesIndex;

Dari perubahan kode di atas, pertama kita import provider Link dari React Router DOM. Ini akan digunakan untuk melakukan navigasi ke halaman-haman.

//import Link from react router dom
import { Link } from "react-router-dom";

Setelah itu, kita buat sebuah state baru untuk pencarian. State ini akan digunakan untuk menyimpan data yang diinputkan di dalam form.

//state search
const [search, setSearch] = useState("");

Dan di dalam function fetchData kita tambahkan sebuah parameter searchData.

//function "fetchData"
const fetchData = async (searchData) => {

	//...
	
}

Dan di dalam function tersebut kita buat variable yang bernama searchQuery yang berisi kondisi menggunakan ternary oprator.

//define variable "searchQuery"
const searchQuery = searchData ? searchData : search;

Dan kita akan gunakan variable tersebut untuk parameter fetching ke dalam Rest API.

//fetching data from Rest API
await Api.get(`/api/admin/places?q=${searchQuery}`, {

	//...

})

Setelah itu, kita buat function baru dengan nama searchHandler. Fungsi tersebut akan dijalankan ketika form untuk pencarian disubmit.

<form onSubmit={searchHandlder} className="form-group">

	//...
	
</form>
//function "searchHandler"
const searchHandlder = (e) => {
    e.preventDefault();

    //call function "fetchDataPost"
    fetchData(search)
}

Di dalam function searchHandler di atas kita memanggil function fetchData dengan menambahkan parameter state search.

Sekarang silahkan teman-teman coba lakukan proses pencarian di dalam halaman places index.

Langkah 3 - Membuat Fitur Pagination

Kita lanjutkan untuk menambahkan fitur pagination. Dimana kita akan menampilkan navigasi nomor untuk berpindah-pindah antar halaman.

karena sebelumnya kita sudah pernah membuat component untuk pagination, maka kita akan import lalu menggunakan component tersebut (reusable).

Silahkan buka file src/pages/admin/places/Index.jsx, kemudian ubah semua kode-nya menjadi seperti berikut ini :

//import react
import React, { useState, useEffect } from "react";

//import layout
import LayoutAdmin from "../../../layouts/Admin";

//import BASE URL API
import Api from "../../../api";

//import js cookie
import Cookies from "js-cookie";

//import Link from react router dom
import { Link } from "react-router-dom";

//import pagination component
import PaginationComponent from "../../../components/utilities/Pagination";

function PlacesIndex() {

    //title page
    document.title = "Places - Administrator Travel GIS";

    //state places
    const [places, setPlaces] = useState([]);

    //state currentPage
    const [currentPage, setCurrentPage] = useState(1);

    //state perPage
    const [perPage, setPerPage] = useState(0);

    //state total
    const [total, setTotal] = useState(0);

    //state search
    const [search, setSearch] = useState("");

    //token
    const token = Cookies.get("token");

    //function "fetchData"
    const fetchData = async (pageNumber, searchData) => {

        //define variable "page"
        const page = pageNumber ? pageNumber : currentPage;

        //define variable "searchQuery"
        const searchQuery = searchData ? searchData : search;

        //fetching data from Rest API
        await Api.get(`/api/admin/places?q=${searchQuery}&page=${page}`, {
            headers: {
                //header Bearer + Token
                Authorization: `Bearer ${token}`,
            }
        }).then(response => {
            //set data response to state "places"
            setPlaces(response.data.data.data);

            //set currentPage
            setCurrentPage(response.data.data.current_page);

            //set perPage
            setPerPage(response.data.data.per_page);

            //total
            setTotal(response.data.data.total);
        });
    };

    //hook
    useEffect(() => {
        //call function "fetchData"
        fetchData();

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    //function "searchHandler"
    const searchHandlder = (e) => {
        e.preventDefault();

        //call function "fetchDataPost"
        fetchData(1, search)
    }

    return (
        <React.Fragment>
            <LayoutAdmin>
              <div className="row mt-4">
                <div className="col-12">
                  <div className="card border-0 rounded shadow-sm border-top-success">
                    <div className="card-header">
                        <span className="font-weight-bold"><i className="fa fa-map-marked-alt"></i> PLACES</span>
                    </div>
                    <div className="card-body">

                        <form onSubmit={searchHandlder} className="form-group">
                            <div className="input-group mb-3">
                                <Link to="/admin/places/create" className="btn btn-md btn-success"><i className="fa fa-plus-circle"></i> ADD NEW</Link>
                                <input type="text" className="form-control" value={search} onChange={(e) => setSearch(e.target.value)} placeholder="search by place title" />
                                <button type="submit" className="btn btn-md btn-success"><i className="fa fa-search"></i> SEARCH</button>
                            </div>
                        </form>
                      
                        <div className="table-responsive">
                            <table className="table table-bordered table-striped table-hovered">
                                <thead>
                                <tr>
                                    <th scope="col">No.</th>
                                    <th scope="col">Title</th>
                                    <th scope="col">Category</th>
                                    <th scope="col">Actions</th>
                                </tr>
                                </thead>
                                <tbody>
                                {places.map((place, index) => (
                                    <tr key={index}>
                                        <td className="text-center">{++index + (currentPage-1) * perPage}</td>
                                        <td>{place.title}</td>
                                        <td>{place.category.name}</td>
                                        <td className="text-center"></td>
                                    </tr>
                                ))}
                                </tbody>
                            </table>
                        </div>
                        <PaginationComponent 
                            currentPage={currentPage} 
                            perPage={perPage} 
                            total={total} 
                            onChange={(pageNumber) => fetchData(pageNumber)}
                            position="end"
                        />
                    </div>
                  </div>
                </div>
              </div>
            </LayoutAdmin>
        </React.Fragment>
    );
}

export default PlacesIndex;

Dari perubahan kode di atas, pertama kita import component pagination terlebih dahulu.

//import pagination component
import PaginationComponent from "../../../components/utilities/Pagination";

Setelah itu, di dalam function fetchData kita tambahkan 1 parameter baru lagi, yaitu pageNumber.

//function "fetchData"
const fetchData = async (pageNumber, searchData) => {

	//...
	
}

Di dalamnya kita buat variable dengan nama page dengan jenis ternary operator.

//define variable "page"
const page = pageNumber ? pageNumber : currentPage;

Kemudian kita gunakan variable tersebut untuk parameter saat proses fetching data ke Rest API.

//fetching data from Rest API
await Api.get(`/api/admin/places?q=${searchQuery}&page=${page}`, {

	//...
	
})

Dan di dalam function searchHanlder kita ubah dengan menambahkan default page 1 saat proses pencarian.

//call function "fetchDataPost"
fetchData(1, search)

Dan untuk menampilkan pagination, kita cukup memanggil component Pagination dan menambahkan data props di dalamnya.

<PaginationComponent 
    currentPage={currentPage} 
    perPage={perPage} 
    total={total} 
    onChange={(pageNumber) => fetchData(pageNumber)}
    position="end"
/>

Di atas, kita mengirimkan beberapa data sebagai props, yaitu currentPage, perPage, total, onChange dan position.

Sekarang, jika kita reload/refresh halaman-nya, maka kita akan mendapatkan hasil kurang lebih seperti berikut ini :

Langkah 4 - Membuat Proses Hapus Data

Sekarang kita akan lanjutkan untuk membuat fitur hapus data, konsepnya yaitu kita akan menambahkan 1 button yang mengarah ke dalam sebuah function dan di dalam function tersebut kita akan melakukan proses delete data ke dalam server menggunakan Rest API.

Sebelum data dihapus, kita akan membuat sebuah jendela konfirmasi atau alert untuk memastikan apakah kita yakin ingin menghapus-nya atau tidak. Dan untuk membuat alert tersebut kita membutuhkan package yang bernama React Confirm Alert.

Silahkan buka file src/pages/admin/places/Index.jsx, kemudian ubah semua kode-nya menjadi seperti berikut ini :

//import react
import React, { useState, useEffect } from "react";

//import layout
import LayoutAdmin from "../../../layouts/Admin";

//import BASE URL API
import Api from "../../../api";

//import js cookie
import Cookies from "js-cookie";

//import Link from react router dom
import { Link } from "react-router-dom";

//import pagination component
import PaginationComponent from "../../../components/utilities/Pagination";

//import toats
import toast from "react-hot-toast";

//import react-confirm-alert
import { confirmAlert } from 'react-confirm-alert';

//import CSS react-confirm-alert
import 'react-confirm-alert/src/react-confirm-alert.css'; // Import css

function PlacesIndex() {

    //title page
    document.title = "Places - Administrator Travel GIS";

    //state places
    const [places, setPlaces] = useState([]);

    //state currentPage
    const [currentPage, setCurrentPage] = useState(1);

    //state perPage
    const [perPage, setPerPage] = useState(0);

    //state total
    const [total, setTotal] = useState(0);

    //state search
    const [search, setSearch] = useState("");

    //token
    const token = Cookies.get("token");

    //function "fetchData"
    const fetchData = async (pageNumber, searchData) => {

        //define variable "page"
        const page = pageNumber ? pageNumber : currentPage;

        //define variable "searchQuery"
        const searchQuery = searchData ? searchData : search;

        //fetching data from Rest API
        await Api.get(`/api/admin/places?q=${searchQuery}&page=${page}`, {
            headers: {
                //header Bearer + Token
                Authorization: `Bearer ${token}`,
            }
        }).then(response => {
            //set data response to state "places"
            setPlaces(response.data.data.data);

            //set currentPage
            setCurrentPage(response.data.data.current_page);

            //set perPage
            setPerPage(response.data.data.per_page);

            //total
            setTotal(response.data.data.total);
        });
    };

    //hook
    useEffect(() => {
        //call function "fetchData"
        fetchData();

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    //function "searchHandler"
    const searchHandlder = (e) => {
        e.preventDefault();

        //call function "fetchDataPost"
        fetchData(1, search)
    }

    //function "deletePlace"
    const deletePlace = (id) => {

        //show confirm alert
        confirmAlert({
            title: 'Are You Sure ?',
            message: 'want to delete this data ?',
            buttons: [{
                    label: 'YES',
                    onClick: async () => {
                        await Api.delete(`/api/admin/places/${id}`, {
                                headers: {
                                    //header Bearer + Token
                                    Authorization: `Bearer ${token}`,
                                }
                            })
                            .then(() => {

                                //show toast
                                toast.success("Data Deleted Successfully!", {
                                    duration: 4000,
                                    position: "top-right",
                                    style: {
                                        borderRadius: '10px',
                                        background: '#333',
                                        color: '#fff',
                                    },
                                });

                                //call function "fetchData"
                                fetchData();
                            })
                    }
                },
                {
                    label: 'NO',
                    onClick: () => {}
                }
            ]
        });
    }

    return (
        <React.Fragment>
            <LayoutAdmin>
              <div className="row mt-4">
                <div className="col-12">
                  <div className="card border-0 rounded shadow-sm border-top-success">
                    <div className="card-header">
                        <span className="font-weight-bold"><i className="fa fa-map-marked-alt"></i> PLACES</span>
                    </div>
                    <div className="card-body">

                        <form onSubmit={searchHandlder} className="form-group">
                            <div className="input-group mb-3">
                                <Link to="/admin/places/create" className="btn btn-md btn-success"><i className="fa fa-plus-circle"></i> ADD NEW</Link>
                                <input type="text" className="form-control" value={search} onChange={(e) => setSearch(e.target.value)} placeholder="search by place title" />
                                <button type="submit" className="btn btn-md btn-success"><i className="fa fa-search"></i> SEARCH</button>
                            </div>
                        </form>
                      
                        <div className="table-responsive">
                            <table className="table table-bordered table-striped table-hovered">
                                <thead>
                                <tr>
                                    <th scope="col">No.</th>
                                    <th scope="col">Title</th>
                                    <th scope="col">Category</th>
                                    <th scope="col">Actions</th>
                                </tr>
                                </thead>
                                <tbody>
                                {places.map((place, index) => (
                                    <tr key={index}>
                                        <td className="text-center">{++index + (currentPage-1) * perPage}</td>
                                        <td>{place.title}</td>
                                        <td>{place.category.name}</td>
                                        <td className="text-center">
                                            <button onClick={() => deletePlace(place.id)} className="btn btn-sm btn-danger"><i className="fa fa-trash"></i></button>
                                        </td>
                                    </tr>
                                ))}
                                </tbody>
                            </table>
                        </div>
                        <PaginationComponent 
                            currentPage={currentPage} 
                            perPage={perPage} 
                            total={total} 
                            onChange={(pageNumber) => fetchData(pageNumber)}
                            position="end"
                        />
                    </div>
                  </div>
                </div>
              </div>
            </LayoutAdmin>
        </React.Fragment>
    );
}

export default PlacesIndex;

Dari perubahan kode di atas, pertama kita import React Hot Toast dan component beserta CSS dari React Confirm Alert.

//import toats
import toast from "react-hot-toast";

//import react-confirm-alert
import { confirmAlert } from 'react-confirm-alert';

//import CSS react-confirm-alert
import 'react-confirm-alert/src/react-confirm-alert.css'; // Import css

Setelah itu, kita menambahkan button delete data di dalam table.

<td className="text-center">
   <button onClick={() => deletePlace(place.id)} className="btn btn-sm btn-danger"><i className="fa fa-trash"></i></button>
</td>

Jika kita perhatikan, di dalam button tersebut kita menambahkan sebuah event onClick yang mengarah ke dalam function yang bernama deletePlace dan di dalamnya kita berikan parameter ID dari data place.

//function "deletePlace"
const deletePlace = (id) => {

	//...
	
}

Dan di dalam function deletePlace, pertama-tama kita akan menampilkan sebuah alert menggunakan React Confirm Alert.

//show confirm alert
confirmAlert({

	//...
	
})

Di dalam confirmAlert kita menambahkan 2 button, yaitu YES dan NO. Jika kita melakukan klik ke button YES, maka akan menjalankan proses delete ke dalam server menggunakan Rest API berdasarkan parameter ID.

await Api.delete(`/api/admin/places/${id}`, {
    headers: {
        //header Bearer + Token
        Authorization: `Bearer ${token}`,
    }
})

Jika proses delete berhasil dilakukan, maka kita akan menampilkan notifikasi menggunakan React Hot Toast yang berisi informasi data berhasil didelete.

//show toast
toast.success("Data Deleted Successfully!", {
    duration: 4000,
    position: "top-right",
    style: {
        borderRadius: '10px',
        background: '#333',
        color: '#fff',
    },
});

Kemudian kita panggil function fetchData lagi dengan tujuan agar data diperbaru setelah berhasil dilakukan proses delete data.

//call function "fetchData"
fetchData();

Tapi, jika button NO yang diklik, maka hanya akan menjalankan function kosong.

onClick: () => {}

Sekarang kita bisa melakukan reload halaman places index, jika berhasil maka kita akan mendapatkan button delete yang kurang lebih seperti berikut ini :

Jika kita klik button tersebut, maka akan menampilkan alert kurang lebih seperti berikut ini :

Konfigurasi Private Route Place Create


Setelah berhasil menampilkan data places, sekarang kita akan lanjutkan belajar bagaiamana cara membuat proses insert data place. Tapi, sebelum itu kita akan melakukan konfigurasi private route-nya terlebih dahulu.

Langkah 1 - Membuat View/Component Place Create

Silahkan buat file baru dengan nama Create.jsx, di dalam folder src/pages/admin/places/. Setelah itu masukkan kode berikut ini di dalamnya.

//import react  
import React from "react";

//import layout admin
import LayoutAdmin from "../../../layouts/Admin";

function PlaceCreate() {

    return(
        <React.Fragment>
            <LayoutAdmin>
                <div className="row mt-4">
                    <div className="col-12">
                        <div className="card border-0 rounded shadow-sm border-top-success">
                            <div className="card-header">
                                <span className="font-weight-bold"><i className="fa fa-map-marked-alt"></i> ADD NEW PLACE</span>
                            </div>
                        </div>
                    </div>
                </div>
            </LayoutAdmin>
        </React.Fragment>
    )

}

export default PlaceCreate

Di atas kita menambahkan sample kode untuk halaman place create, dimateri selanjutnya kita akan ubah dan sesuai dengan kebutuhan kita.

Langkah 2 - Konfigurasi Private Route Place Create

Setelah berhasil membuat view/component untuk halaman place create, selanjutnya kita akan belajar melakukan konfigurasi private route-nya.

Silahkan buka file src/routes/routes.jsx, kemudian ubah kode-nya menjadi seperti berikut ini :

//import react router dom
import { Routes, Route } from "react-router-dom";

//=======================================================================
//ADMIN
//=======================================================================

//import view Login
import Login from '../pages/admin/Login.jsx';

//import component private routes
import PrivateRoute from "./PrivateRoutes";

//import view admin Dashboard
import Dashboard from '../pages/admin/dashboard/Index.jsx';

//import view admin categories Index
import CategoriesIndex from '../pages/admin/categories/Index.jsx';

//import view admin category Create
import CategoryCreate from '../pages/admin/categories/Create.jsx';

//import view admin category Edit
import CategoryEdit from '../pages/admin/categories/Edit.jsx';

//import view admin places Index
import PlacesIndex from '../pages/admin/places/Index.jsx';

//import view admin places Create
import PlaceCreate from '../pages/admin/places/Create.jsx';

function RoutesIndex() {
    return (
        <Routes>

            {/* route "/admin/login" */}
            <Route path="/admin/login" element={<Login />} />

            {/* private route "/admin/dashboard" */}
            <Route
                path="/admin/dashboard"
                element={
                        <PrivateRoute>
                            <Dashboard />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories" */}
            <Route
                path="/admin/categories"
                element={
                        <PrivateRoute>
                            <CategoriesIndex />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories/create" */}
            <Route
                path="/admin/categories/create"
                element={
                        <PrivateRoute>
                            <CategoryCreate />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories/edit/:id" */}
            <Route
                path="/admin/categories/edit/:id"
                element={
                        <PrivateRoute>
                            <CategoryEdit />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/places" */}
            <Route
                path="/admin/places"
                element={
                        <PrivateRoute>
                            <PlacesIndex />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/places/create" */}
            <Route
                path="/admin/places/create"
                element={
                        <PrivateRoute>
                            <PlaceCreate />
                        </PrivateRoute>
                }
            />

        </Routes>
    )
}

export default RoutesIndex

Dari perubahan kode di atas, pertama kita melakukan import view/component place create.

//import view admin places Create
import PlaceCreate from '../pages/admin/places/Create.jsx';

Setelah itu, kita definisikan private route-nya seperti berikut ini :

{/* private route "/admin/places/create" */}
<Route
    path="/admin/places/create"
    element={
    <PrivateRoute>
        <PlaceCreate />
    </PrivateRoute>
    }
/>

Di atas, untuk path dari route-nya adalah /admin/places/create. Dan jika URL tersebut diakses, maka akan memanggil PlaceCreate atau view/component place create.

Sekarang, jika kita klik button ADD NEW di halaman places index atau ke URL berikut ini http://localhost:5173/admin/places/create, maka jika berhasil akan mendapatkan hasil seperti berikut ini :

Membuat Proses Create Data Place


Dimateri sebelumnya kita telah belajar bagaimana cara membuat private route untuk menampilkan halaman place create dan sekarang kita akan lanjutkan untuk melakukan perubahan di dalam halaman tersebut dengan menampilkan form create data dan sekaligus membuat proses insert data ke dalam server menggunakan Rest API. Disini kita juga akan belajar melakukan multiple upload gambar tanpa batas ke dalam server.

Langkah 1 - Edit View/Component Place Create

Sekarang kita akan melakukan perubahan di dalam view/component yang sudah pernah kita buat sebelumnya. Silahkan buka file src/pages/admin/places/Create.jsx, kemudian ubah semua kode-nya menjadi seperti berikut ini :

//import hook from react
import React, { useState, useEffect } from "react";

//import layout
import LayoutAdmin from "../../../layouts/Admin";

//import BASE URL API
import Api from "../../../api";

//import hook navigate dari react router dom
import { useNavigate } from "react-router-dom";

//import js cookie
import Cookies from "js-cookie";

//import toats
import toast from "react-hot-toast";

//import react Quill
import ReactQuill from 'react-quill';

// quill CSS
import 'react-quill/dist/quill.snow.css';

function PlaceCreate() {

	//title page
    document.title = "Add New Place - Administrator Travel GIS";

    //state form
    const [title, setTitle] = useState("");
    const [categoryID, setCategoryID] = useState("");
    const [description, setDescription] = useState("");
    const [phone, setPhone] = useState("");
    const [website, setWebsite] = useState("");
    const [office_hours, setOfficeHours] = useState("");
    const [address, setAddress] = useState("");
    const [latitude, setLatitude] = useState("");
    const [longitude, setLongitude] = useState("");

    //state image array / multiple
    const [images, setImages] = useState([]);

    //state categories
    const [categories, setCategories] = useState([]);

    //state validation
    const [validation, setValidation] = useState({});

    //token
    const token = Cookies.get("token");

    //navigate
    const navigate = useNavigate();

    //function "fetchCategories"
    const fetchCategories = async () => {

        //fetching data from Rest API
        await Api.get('/api/web/categories')
        .then(response => {
        	//set data response to state "catgeories"
        	setCategories(response.data.data);
        });

    }

    //hook
    useEffect(() => {
        //call function "fetchCategories"
        fetchCategories();
    }, []);

    //function "handleFileChange"
    const handleFileChange = (e) => {
        
        //define variable for get value image data
        const imageData = e.target.files;

        Array.from(imageData).forEach(image => {
        	//check validation file
            if(!image.type.match('image.*')) {

                setImages([]);

                //show toast
                toast.error("Format File not Supported!", {
                    duration: 4000,
                    position: "top-right",
                    style: {
                        borderRadius: '10px',
                        background: '#333',
                        color: '#fff',
                    },
                });

                return
            } else {
                setImages([...e.target.files]);
            }
        });
        
    }

    //function "storePlace"
    const storePlace = async (e) => {
        e.preventDefault();

        //define formData
        const formData = new FormData();

        //append data to "formData"
        formData.append('title', title);
        formData.append('category_id', categoryID);
        formData.append('description', description);
        formData.append('phone', phone);
        formData.append('website', website);
        formData.append('office_hours', office_hours);
        formData.append('address', address);
        formData.append('latitude', latitude);
        formData.append('longitude', longitude);

        Array.from(images).forEach(image => {
            formData.append("image[]", image);
        });

        //send data to server
        await Api.post('/api/admin/places', formData, {
            
            //header
            headers: {
                //header Bearer + Token
                'Authorization': `Bearer ${token}`,
                'content-type': 'multipart/form-data'
            }
            
        }).then(() => {

            //show toast
            toast.success("Data Saved Successfully!", {
                duration: 4000,
                position: "top-right",
                style: {
                    borderRadius: '10px',
                    background: '#333',
                    color: '#fff',
                },
            });

            //redirect dashboard page
            navigate("/admin/places");

        })
        .catch((error) => {
            
            //set state "validation"
            setValidation(error.response.data);
        })

    }

    return (
        <React.Fragment>
            <LayoutAdmin>
                <div className="row mt-4 mb-5">
                    <div className="col-12">
                        <div className="card border-0 rounded shadow-sm border-top-success">
                            <div className="card-header">
                                <span className="font-weight-bold"><i className="fa fa-map-marked-alt"></i> ADD NEW PLACE</span>
                            </div>
                            <div className="card-body">
                                <form onSubmit={storePlace}>
                                    <div className="mb-3">
                                        <label className="form-label fw-bold">Image (<i>select many file</i>)</label>
                                        <input type="file" className="form-control" onChange={handleFileChange} multiple/>
                                    </div>
                                    <div className="mb-3">
                                        <label className="form-label fw-bold">Title</label>
                                        <input type="text" className="form-control" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Enter Title Place"/>
                                    </div>
                                    {validation.title && (
                                        <div className="alert alert-danger">
                                            {validation.title[0]}
                                        </div>
                                    )}
                                    <div className="row">
                                        <div className="col-md-6">
                                        <div className="mb-3">
                                            <label className="form-label fw-bold">Category</label>
                                            <select className="form-select" value={categoryID} onChange={(e) => setCategoryID(e.target.value)}>
                                                <option value="">-- Select Category --</option>
                                                {
                                                categories.map((category) => (
                                                    <option value={category.id} key={category.id}>{category.name}</option>
                                                ))
                                                }
                                            </select>
                                        </div>
                                        {validation.category_id && (
                                            <div className="alert alert-danger">
                                                {validation.category_id[0]}
                                            </div>
                                        )}
                                        </div>
                                        <div className="col-md-6">
                                        <div className="mb-3">
                                            <label className="form-label fw-bold">Office Hours</label>
                                            <input type="text" className="form-control" value={office_hours} onChange={(e) => setOfficeHours(e.target.value)} placeholder="Enter Office Hours"/>
                                        </div>
                                        {validation.office_hours && (
                                            <div className="alert alert-danger">
                                                {validation.office_hours[0]}
                                            </div>
                                        )}
                                        </div>
                                    </div>
                                    <div className="mb-3">
                                        <label className="form-label fw-bold">Description</label>
                                        <ReactQuill theme="snow" rows="5" value={description} onChange={(content) => setDescription(content)}/>
                                    </div>
                                    {validation.description && (
                                        <div className="alert alert-danger">
                                            {validation.description[0]}
                                        </div>
                                    )}
                                    <div className="row">
                                        <div className="col-md-6">
                                        <div className="mb-3">
                                            <label className="form-label fw-bold">Phone</label>
                                            <input type="text" className="form-control" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="Enter Phone"/>
                                        </div>
                                        {validation.phone && (
                                            <div className="alert alert-danger">
                                                {validation.phone[0]}
                                            </div>
                                        )}
                                        </div>
                                        <div className="col-md-6">
                                        <div className="mb-3">
                                            <label className="form-label fw-bold">Website</label>
                                            <input type="text" className="form-control" value={website} onChange={(e) => setWebsite(e.target.value)} placeholder="Enter Website Place"/>
                                        </div>
                                        {validation.website && (
                                            <div className="alert alert-danger">
                                                {validation.title[0]}
                                            </div>
                                        )}
                                        </div>
                                    </div>
                                    <div className="mb-3">
                                        <label className="form-label fw-bold">Address</label>
                                        <textarea className="form-control" rows="3" value={address} onChange={(e) => setAddress(e.target.value)} placeholder="Enter Address Place"></textarea>
                                    </div>
                                    {validation.address && (
                                        <div className="alert alert-danger">
                                            {validation.address[0]}
                                        </div>
                                    )}
                                    <div className="row">
                                        <div className="col-md-6">
                                        <div className="mb-3">
                                            <label className="form-label fw-bold">Latitude</label>
                                            <input type="text" className="form-control" value={latitude} onChange={(e) => setLatitude(e.target.value)} placeholder="Latitude Place"/>
                                        </div>
                                        {validation.latitude && (
                                            <div className="alert alert-danger">
                                                {validation.latitude[0]}
                                            </div>
                                        )}
                                        </div>
                                        <div className="col-md-6">
                                        <div className="mb-3">
                                            <label className="form-label fw-bold">Longitude</label>
                                            <input type="text" className="form-control" value={longitude} onChange={(e) => setLongitude(e.target.value)} placeholder="Longitude Place"/>
                                        </div>
                                        {validation.longitude && (
                                            <div className="alert alert-danger">
                                                {validation.longitude[0]}
                                            </div>
                                        )}
                                        </div>
                                    </div>
                                    <div>
                                        <button type="submit" className="btn btn-md btn-success me-2"><i className="fa fa-save"></i> SAVE</button>
                                        <button type="reset" className="btn btn-md btn-warning"><i className="fa fa-redo"></i> RESET</button>
                                    </div>
                                </form>
                            </div>
                        </div>
                    </div>
                </div>
            </LayoutAdmin>
        </React.Fragment>
    );
}

export default PlaceCreate;

Dari perubahan kode di atas, pertama kita import hook dari react, yaitu useState dan useEffect.

//import hook from react
import React, { useState, useEffect } from "react";

Setelah itu, kita import konfigurasi endpoint, karena kita akan gunakan untuk melakukan HTTP request ke dalam server.

//import BASE URL API
import Api from "../../../api";

Kemudian kita import hook useNavigate dari React Router DOM. Hook ini akan kita gunakan untuk melakukan redirect atau navigate ke halaman lain.

//import hook navigate dari react router dom
import { useNavigate } from "react-router-dom";

Karena akan menggunakan token dari cookies, maka kita akan gunakan package Js Cookie untuk mepermudah dalam melakukan manajemen cookies di dalam browser.

//import js cookie
import Cookies from "js-cookie";

Setelah itu kita akan import package React Hot Toast, dimana akan kita gunakan untuk menampilkan notifikasi setelah berhasil melakukan poroses insert data nanti-nya.

//import toats
import toast from "react-hot-toast";

Di dalam form nanti kita juga akan menampilkan Text Editor atau biasa diusebut dengan WYSIWYG, disini kita akan menggunakan package yang bernama React Quill.

//import react Quill
import ReactQuill from 'react-quill';

// quill CSS
import 'react-quill/dist/quill.snow.css';

Di dalam function component PlaceCreate kita mendefinisikan title untuk halaman ini.

//title page
document.title = "Add New Place - Administrator Travel GIS";

Selanjutnya kita membuat beberapa state yang akan digunakan untuk menyimpan data yang di inputkan di dalam form.

//state form
const [title, setTitle] = useState("");
const [categoryID, setCategoryID] = useState("");
const [description, setDescription] = useState("");
const [phone, setPhone] = useState("");
const [website, setWebsite] = useState("");
const [office_hours, setOfficeHours] = useState("");
const [address, setAddress] = useState("");
const [latitude, setLatitude] = useState("");
const [longitude, setLongitude] = useState("");

Setelah itu, kita juga buat state lagi untuk menyimpan data gambar. Disini kita atur state-nya dengan jenis array, karena datanya bisa lebih dari satu atau jamak.

//state image array / multiple
const [images, setImages] = useState([]);

Kemudian ada state lagi yang akan kita gunakan untuk menyimpan data categories. Dan jenis-nya juga sama yaitu array, karena datanya akan lebih dari satu.

//state categories
const [categories, setCategories] = useState([]);

Dan state terakhir kita gunakan untuk menyimpan error validation dari Rest API dan state ini menggunakan jenis object.

//state validation
const [validation, setValidation] = useState({});

Untuk mempermudah dalam menggunakan token, maka kita buat sebuah variable baru dengan nama token yang isinya adalah token yang diambil dari cookies browser.

//token
const token = Cookies.get("token");

Dan untuk mempermudah dalam menggunakan hook useNavigate, maka kita letakkan hook tersebut di dalam variable.

//navigate
const navigate = useNavigate();

kemudian kita buat sebuah function dengan jenis asynchronus dengan nama fetchCategories.

//function "fetchCategories"
const fetchCategories = async () => {

	//...
	
}

Function tersebut akan kita gunakan untuk melakukan HTTP request ke dalam server untuk mendapatkan data categories dan kita tampilkan di dalam form sebagai dropdown.

//fetching data from Rest API
await Api.get('/api/web/categories')

Jika proses fetching di atas berhasil, maka kita akan assign response data-nya ke dalam state categories.

//set data response to state "catgeories"
setCategories(response.data.data);

Dan agar function fecthCategories dapat dijalankan saat component diload, maka kita perlu memanggilnya di dalam hook useEffect.

//hook
useEffect(() => {
    //call function "fetchCategories"
    fetchCategories();
}, []);

Dan untuk menampilkan data categories di dalam form, kita menggunakan perulangan map. Kurang lebih seperti berikut ini :

<select className="form-select" value={categoryID} onChange={(e)=> setCategoryID(e.target.value)}>
     <option value="">-- Select Category --</option>
     {
     	categories.map((category) => (
     		<option value={category.id} key={category.id}>{category.name}</option>
     	))
     }
</select>

Setelah itu, kita buat function yang bernama handleFileChange. Fungsi tersebut akan kita gunakan untuk memeriksa file gambar yang akan diupload, apakah sudah sesuai atau belum.

//function "handleFileChange"
const handleFileChange = (e) => {

	//...
	
}

Di dalam function tersebut, pertama kita buat variable dengan nama imageData yang berisin data gambar (multiple) yang dikirimkan melalui form.

//define variable for get value image data
const imageData = e.target.files;

Setelah itu, kita akan lakukan perulangan untuk memeriksa ektensi dari masing-masing file yang akan diupload.

Array.from(imageData).forEach(image => {

	//...

}

Jika ada salah satu file yang tidak memiliki ekstensi gambar, maka kita akan menampilkan notifikasi error menggukan React Hot Toast yang berisi informasi format file tidak disupport.

if (!image.type.match('image.*')) {

    setImages([]);

    //show toast
    toast.error("Format File not Supported!", {
        duration: 4000,
        position: "top-right",
        style: {
            borderRadius: '10px',
            background: '#333',
            color: '#fff',
        },
    });

    return
}

Tapi jika file yang akan diupload sudah sesuai, maka kita akan assign ke dalam state images menggunakan Spread Operator.

setImages([...e.target.files]);

Kemudian kita buat function lagi dengan nama storePlace. Fungsi ini akan dijalankan ketika form disubmit.

<form onSubmit={storePlace}>

	//...
	
</form>

Saat form disubmit, maka akan menjalankan function yang bernama storePlace.

//function "storePlace"
const storePlace = async (e) => {

	//...
	
}

Di dalam function tersebut, pertama-tama kita melakukan inisialisasi formData.

//define formData
const formData = new FormData();

Setelah berhasil meng-inisialisasi formData, maka selanjutnya kita akan melakukan append data ke dalam formData yang nantinya akan dikirimkan ke dalam server.

//append data to "formData"
formData.append('title', title);
formData.append('category_id', categoryID);
formData.append('description', description);
formData.append('phone', phone);
formData.append('website', website);
formData.append('office_hours', office_hours);
formData.append('address', address);
formData.append('latitude', latitude);
formData.append('longitude', longitude);

Array.from(images).forEach(image => {
    formData.append("image[]", image);
});

Di atas, kita melakukan append beberapa data dan untuk gambar karena memiliki jumlah yang tak terbatas, maka kita menggunakan perulangan forEach untuk append-nya.

Setelah data berhasil dimasukkan di dalam formData, maka sekarang kita tinggal mengirimkannya ke dalam server menggunakan Rest API.

//send data to server
await Api.post('/api/admin/places', formData, {

    //header
    headers: {
        //header Bearer + Token
        'Authorization': `Bearer ${token}`,
        'content-type': 'multipart/form-data'
    }

})

Jika proses insert data berhasil dilakukan di dalam server, maka kita akan menampilkan sebuah notifikasi menggunakan React Hot Toast yang berisi informasi data berhasikl disimpan.

//show toast
toast.success("Data Saved Successfully!", {
    duration: 4000,
    position: "top-right",
    style: {
        borderRadius: '10px',
        background: '#333',
        color: '#fff',
    },
});

Setelah itu, kita arahkan atau redirect ke dalam halaman places index.

//redirect dashboard page
navigate("/admin/places");

Tapi, jika proses insert data gagal dilakukan, maka akan melakukan assign error response validasi ke dalam state validation.

//set state "validation"
setValidation(error.response.data);

Langkag 2 - Uji Coba Proses Insert Data

Sekarang kita akan melakukan proses uji coba insert data place baru. Silahkan klik button ADD NEW yang ada di dalam halaman places index atau bisa ke URL berikut ini http://localhost:5173/admin/places/create, jika berhasil maka akan menampilkan halaman seperti berikut ini :

Silahkan diisi sesuai dengan keinginan, dan jika berhasil maka kita akan mendapatkan notifikasi data berhasil disimpan dan kita akan diarahkan ke dalam halaman places index.

Menambahkan Mapbox Geocoder di Proses Create Data Place


Setelah berhasil melakukan proses insert data ke dalam server, maka sekarang kita akan belajar menampilkan maps dan menambahkan sebuah pencarian di dalam maps tersebut atau biasa disebut dengan geocoder.

Jika kita perhatikan di dalam halaman create place sebelumnya, untuk input longitude dan latitude kita masih manual, dan sekarang kita akan integrasikan dengan mapbox dan geocoder agar bisa otomatis mengambil nilai longitude dan latitude dari sebuah maps.

Langkah 1 - Edit View/Component Place Create

Sekarang kita akan melakukan penambahan kode untuk integrasi mapbox di dalam halaman place create. Silahkan buka file src/pages/admin/places/Create.jsx, kemudian ubah semua kode-nya menjadi seperti berikut ini :

//import hook from react
import React, { useState, useEffect, useRef } from "react";

//import layout
import LayoutAdmin from "../../../layouts/Admin";

//import BASE URL API
import Api from "../../../api";

//import hook navigate dari react router dom
import { useNavigate } from "react-router-dom";

//import js cookie
import Cookies from "js-cookie";

//import toats
import toast from "react-hot-toast";

//import react Quill
import ReactQuill from 'react-quill';

// quill CSS
import 'react-quill/dist/quill.snow.css';

//mapbox gl
import mapboxgl from 'mapbox-gl'; // eslint-disable-line import/no-webpack-loader-syntax

//mapbox gl geocoder
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';

//api key mapbox
mapboxgl.accessToken = import.meta.env.VITE_APP_MAPBOX;

function PlaceCreate() {

	//title page
    document.title = "Add New Place - Administrator Travel GIS";

    //state form
    const [title, setTitle] = useState("");
    const [categoryID, setCategoryID] = useState("");
    const [description, setDescription] = useState("");
    const [phone, setPhone] = useState("");
    const [website, setWebsite] = useState("");
    const [office_hours, setOfficeHours] = useState("");
    const [address, setAddress] = useState("");
    const [latitude, setLatitude] = useState("");
    const [longitude, setLongitude] = useState("");

    //state image array / multiple
    const [images, setImages] = useState([]);

    //state categories
    const [categories, setCategories] = useState([]);

    //state validation
    const [validation, setValidation] = useState({});

    //token
    const token = Cookies.get("token");

    //navigate
    const navigate = useNavigate();

    //function "fetchCategories"
    const fetchCategories = async () => {

        //fetching data from Rest API
        await Api.get('/api/web/categories')
        .then(response => {
        	//set data response to state "catgeories"
        	setCategories(response.data.data);
        });

    }

    //hook
    useEffect(() => {
        //call function "fetchCategories"
        fetchCategories();
    }, []);

    //function "handleFileChange"
    const handleFileChange = (e) => {
        
        //define variable for get value image data
        const imageData = e.target.files;

        Array.from(imageData).forEach(image => {
        	//check validation file
            if(!image.type.match('image.*')) {

                setImages([]);

                //show toast
                toast.error("Format File not Supported!", {
                    duration: 4000,
                    position: "top-right",
                    style: {
                        borderRadius: '10px',
                        background: '#333',
                        color: '#fff',
                    },
                });

                return
            } else {
                setImages([...e.target.files]);
            }
        });
        
    }

    //function "storePlace"
    const storePlace = async (e) => {
        e.preventDefault();

        //define formData
        const formData = new FormData();

        //append data to "formData"
        formData.append('title', title);
        formData.append('category_id', categoryID);
        formData.append('description', description);
        formData.append('phone', phone);
        formData.append('website', website);
        formData.append('office_hours', office_hours);
        formData.append('address', address);
        formData.append('latitude', latitude);
        formData.append('longitude', longitude);

        Array.from(images).forEach(image => {
            formData.append("image[]", image);
        });

        //send data to server
        await Api.post('/api/admin/places', formData, {
            
            //header
            headers: {
                //header Bearer + Token
                'Authorization': `Bearer ${token}`,
                'content-type': 'multipart/form-data'
            }
            
        }).then(() => {

            //show toast
            toast.success("Data Saved Successfully!", {
                duration: 4000,
                position: "top-right",
                style: {
                    borderRadius: '10px',
                    background: '#333',
                    color: '#fff',
                },
            });

            //redirect dashboard page
            navigate("/admin/places");

        })
        .catch((error) => {
            
            //set state "validation"
            setValidation(error.response.data);
        })

    }

    //=========================================================
    //MAPBOX
    //=========================================================
 
    //define state
    const mapContainer = useRef(null);

    useEffect(() => {

        //init map
        const map = new mapboxgl.Map({
            container: mapContainer.current,
            style: 'mapbox://styles/mapbox/streets-v12',
            center: [longitude, latitude],
            zoom: 12
        });

        //init geocoder
        const geocoder = new MapboxGeocoder({
            accessToken: mapboxgl.accessToken,
            
            marker: {
                draggable: true
            },
            
            mapboxgl: mapboxgl
        });

        //add geocoder to map
        map.addControl(geocoder);

        //init marker
        const marker = new mapboxgl.Marker({ 
            draggable: true, 
            color: "rgb(47 128 237)" 
        })
        
        //set longtitude and latitude
        .setLngLat([longitude, latitude])
        //add marker to map
        .addTo(map);
    
    
        //geocoder result
        geocoder.on('result', function(e) {
            
            //remove marker
            marker.remove();
            
            //set longitude and latitude
            marker.setLngLat(e.result.center)

                //add to map
                .addTo(map);
        
            //event marker on dragend
            marker.on('dragend', function (e) {
                
                //assign longitude and latitude to state
                setLongitude(e.target._lngLat.lng)
                setLatitude(e.target._lngLat.lat)
                
            });
            
        });

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    return (
        <React.Fragment>
            <LayoutAdmin>
                <div className="row mt-4 mb-5">
                    <div className="col-12">
                        <div className="card border-0 rounded shadow-sm border-top-success">
                            <div className="card-header">
                                <span className="font-weight-bold"><i className="fa fa-map-marked-alt"></i> ADD NEW PLACE</span>
                            </div>
                            <div className="card-body">
                                <form onSubmit={storePlace}>
                                    <div className="mb-3">
                                        <label className="form-label fw-bold">Image (<i>select many file</i>)</label>
                                        <input type="file" className="form-control" onChange={handleFileChange} multiple/>
                                    </div>
                                    <div className="mb-3">
                                        <label className="form-label fw-bold">Title</label>
                                        <input type="text" className="form-control" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Enter Title Place"/>
                                    </div>
                                    {validation.title && (
                                        <div className="alert alert-danger">
                                            {validation.title[0]}
                                        </div>
                                    )}
                                    <div className="row">
                                        <div className="col-md-6">
                                        <div className="mb-3">
                                            <label className="form-label fw-bold">Category</label>
                                            <select className="form-select" value={categoryID} onChange={(e) => setCategoryID(e.target.value)}>
                                                <option value="">-- Select Category --</option>
                                                {
                                                categories.map((category) => (
                                                    <option value={category.id} key={category.id}>{category.name}</option>
                                                ))
                                                }
                                            </select>
                                        </div>
                                        {validation.category_id && (
                                            <div className="alert alert-danger">
                                                {validation.category_id[0]}
                                            </div>
                                        )}
                                        </div>
                                        <div className="col-md-6">
                                        <div className="mb-3">
                                            <label className="form-label fw-bold">Office Hours</label>
                                            <input type="text" className="form-control" value={office_hours} onChange={(e) => setOfficeHours(e.target.value)} placeholder="Enter Office Hours"/>
                                        </div>
                                        {validation.office_hours && (
                                            <div className="alert alert-danger">
                                                {validation.office_hours[0]}
                                            </div>
                                        )}
                                        </div>
                                    </div>
                                    <div className="mb-3">
                                        <label className="form-label fw-bold">Description</label>
                                        <ReactQuill theme="snow" rows="5" value={description} onChange={(content) => setDescription(content)}/>
                                    </div>
                                    {validation.description && (
                                        <div className="alert alert-danger">
                                            {validation.description[0]}
                                        </div>
                                    )}
                                    <div className="row">
                                        <div className="col-md-6">
                                        <div className="mb-3">
                                            <label className="form-label fw-bold">Phone</label>
                                            <input type="text" className="form-control" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="Enter Phone"/>
                                        </div>
                                        {validation.phone && (
                                            <div className="alert alert-danger">
                                                {validation.phone[0]}
                                            </div>
                                        )}
                                        </div>
                                        <div className="col-md-6">
                                        <div className="mb-3">
                                            <label className="form-label fw-bold">Website</label>
                                            <input type="text" className="form-control" value={website} onChange={(e) => setWebsite(e.target.value)} placeholder="Enter Website Place"/>
                                        </div>
                                        {validation.website && (
                                            <div className="alert alert-danger">
                                                {validation.title[0]}
                                            </div>
                                        )}
                                        </div>
                                    </div>
                                    <div className="mb-3">
                                        <label className="form-label fw-bold">Address</label>
                                        <textarea className="form-control" rows="3" value={address} onChange={(e) => setAddress(e.target.value)} placeholder="Enter Address Place"></textarea>
                                    </div>
                                    {validation.address && (
                                        <div className="alert alert-danger">
                                            {validation.address[0]}
                                        </div>
                                    )}
                                    <div className="row">
                                        <div className="col-md-6">
                                        <div className="mb-3">
                                            <label className="form-label fw-bold">Latitude</label>
                                            <input type="text" className="form-control" value={latitude} onChange={(e) => setLatitude(e.target.value)} placeholder="Latitude Place"/>
                                        </div>
                                        {validation.latitude && (
                                            <div className="alert alert-danger">
                                                {validation.latitude[0]}
                                            </div>
                                        )}
                                        </div>
                                        <div className="col-md-6">
                                        <div className="mb-3">
                                            <label className="form-label fw-bold">Longitude</label>
                                            <input type="text" className="form-control" value={longitude} onChange={(e) => setLongitude(e.target.value)} placeholder="Longitude Place"/>
                                        </div>
                                        {validation.longitude && (
                                            <div className="alert alert-danger">
                                                {validation.longitude[0]}
                                            </div>
                                        )}
                                        </div>
                                        <div className="row mb-3">
                                            <div className="col-md-12">
                                                <div ref={mapContainer} className="map-container" />
                                            </div>
                                        </div>
                                    </div>
                                    <div>
                                        <button type="submit" className="btn btn-md btn-success me-2"><i className="fa fa-save"></i> SAVE</button>
                                        <button type="reset" className="btn btn-md btn-warning"><i className="fa fa-redo"></i> RESET</button>
                                    </div>
                                </form>
                            </div>
                        </div>
                    </div>
                </div>
            </LayoutAdmin>
        </React.Fragment>
    );
}

export default PlaceCreate;

Dari perubahan yang kita laakukan di atas, pertama kita import hook useRef dari React.

//import hook from react
import React, { useState, useEffect, useRef } from "react";

Setelah itu, kita import package Mapbox GL dan Mapbox Geocoder. Untuk Mapbox GL akan kita gunakan menampilkan maps atau peta, sedangkan Mapbox Geocoder akan digunakan untuk menambahkan pencarian di dalam maps.

//mapbox gl
import mapboxgl from 'mapbox-gl'; // eslint-disable-line import/no-webpack-loader-syntax

//mapbox gl geocoder
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';

Kemudian kita konfigurasi access token untuk Mapbox dari file .env.

//api key mapbox
mapboxgl.accessToken = import.meta.env.VITE_APP_MAPBOX;

Dan di dalam function component, pertama-tama kita define variable dengan nama mapContainer dan variable tersebut menggunakan hook useRef. Nantinya akan kita gunakan untuk melakukan render maps.

//define state
const mapContainer = useRef(null);

Kemudian agar maps dapat ditampilkan saat halaman dibuka, maka kita menaruhnya di dalam hook useEffect. Disini kita melakukan inisialisasi mapbox.

//init map
const map = new mapboxgl.Map({
    container: mapContainer.current,
    style: 'mapbox://styles/mapbox/streets-v12',
    center: [longitude, latitude],
    zoom: 12
});

Dari inisialisasi mapbox di atas, kita membuat object dari mapbox yang berisi beberapa konfigurasi, seperti :

  1. container - element HTML untuk merender maps.
  2. style - style maps yang digunakan.
  3. center - berisi longitude dan latitude dari sebuah maps dan kita berikan value dari state.
  4. zoom - jarak pandang maps.

Setelah inisialisasi mapbox, sekarang kita lanjutkan untuk melakukan inisialisasi geocoder.

//init geocoder
const geocoder = new MapboxGeocoder({
    accessToken: mapboxgl.accessToken,

    marker: {
        draggable: true
    },

    mapboxgl: mapboxgl
});

Di dalam object geocoder di atas, pertama kita atur accessToken-nya, kita ambil dari mapboxgl.accessToken. Setelah itu, kita atur marker untuk maps-nya agar bisa di dgrag atau dipindah-pindah.

kemudian kita assign konfigurasi geocoder tersebut ke dalam maps menggunakan addControl. Dengan begitu, maka di dalam maps akan muncul sebuah kolom pencarian.

//add geocoder to map
map.addControl(geocoder);

Selanjutnya kita melakukan inisialisasi markernya dan untuk melakukan inisialisasi marker kita membuat object dari Mapbox GL.

//init marker
const marker = new mapboxgl.Marker({
    draggable: true,
    color: "rgb(47 128 237)"
})

//set longtitude and latitude
.setLngLat([longitude, latitude])
//add marker to map
.addTo(map);

Di atas, kita atur agar marker tersebut dapat dipindah-pindah dan kita berikan warna dengan kode RGB. dan kita set longitude dan latitude yang diambil dari state. Dan terkahir kita assign ke dalam maps dengan method addTo.

Kemudian di dalam geocoder ada event yang bernama result. Event tersebut akan dijalankan ketika kita selesai melakukan sebuah pencarian di dalam maps.

//geocoder result
geocoder.on('result', function(e) {

	//...

}

Di dalam event result tersebut, pertama kita akan menghapus marker yang lama terlebih dahulu.

//remove marker
marker.remove();

Setelah marker yang lama berhasil dihapus, maka sekarang kita akan set untuk marker yang baru dengan hasil response yang berisi longitude dan latitude.

//set longitude and latitude
marker.setLngLat(e.result.center)

//add to map
.addTo(map);

Kemudian di dalam marker ada event yang bernama dragend, yaitu membaca marker di pindahkan atau di drag terakhir.

//event marker on dragend
marker.on('dragend', function (e) {

	//...

}

Di dalam event dragend marker tersebut kita melakukan assign longitude dan latitude dari maps ke dalam state.

//assign longitude and latitude to state
setLongitude(e.target._lngLat.lng)
setLatitude(e.target._lngLat.lat)

Dan di dalam JSX, kita tambahkan mapContainer untuk melakukan render maps-nya.

<div className="row mb-3">
    <div className="col-md-12">
        <div ref={mapContainer} className="map-container" />
    </div>
</div>

Dan di dalam input logitude dan latitude kita berikan attribute readOnly, artinya kita tidak bisa mengisinya secara manual, melainkan akan diisi oleh maps secara otomatis.

<input type="text" className="form-control" readOnly value={latitude} onChange={(e) => setLatitude(e.target.value)} placeholder="Latitude Place"/>

<input type="text" className="form-control" readOnly value={longitude} onChange={(e) => setLongitude(e.target.value)} placeholder="Longitude Place"/>

Langkah 2 - Uji Coba Maps dan Geocoder

Sekarang silahkan reload halaman place create dan jika berhasil maka akan menampilkan sebuah maps dan kolom pencarian.

Silahkan melakukan pencarian di dalam geocoder yang ada di dalam maps tersebut dan jika berhasil maka nilai-nya akan di assign secara otomatis di dalam input longitude dan latitude. Kurang lebih seperti berikut ini :

Konfigurasi Private Route Place Edit


Pada materi kali ini kita akan belajar membuat konfigurasi private route untuk menampilkan halaman form untuk melakukan edit dan update data place. Disini kita akan membuat view/component-nya terlebih dahulu sebelum membuat konfigurasi private route-nya.

Dan akan kita berikan kode sederhana terlebih dahulu di dalam view/component tersebut, dengan tujuan untuk memerika apakah konfigurasi private route-nya sudah sesuai atau belum. Karena akan kita ubah lagi kode yang ada di pembahasan materi berikutnya.

Langkah 1 - Membuat View/Component Place Edit

Pertama-tama kita akan membuat view/component-nya terlebih dahulu untuk halaman edit place. Silahkan buat file baru dengan nama Edit.jsx di dalam folder src/pages/admin/places. Kemudian masukkan kode berikut ini di dalamnya.

//import react  
import React from "react";

//import layout admin
import LayoutAdmin from "../../../layouts/Admin";

function PlaceEdit() {

    return(
        <React.Fragment>
            <LayoutAdmin>
                <div className="row mt-4">
                    <div className="col-12">
                        <div className="card border-0 rounded shadow-sm border-top-success">
                            <div className="card-header">
                                <span className="font-weight-bold"><i className="fa fa-map-marked-alt"></i> EDIT PLACE</span>
                            </div>
                        </div>
                    </div>
                </div>
            </LayoutAdmin>
        </React.Fragment>
    )

}

export default PlaceEdit

Dari penambahan kode di atas, kita memberikan sample kode di dalam halaman edit place. Dan di atas kita melakukan import LayoutAdmin terlebih dahulu. Karena kode yang akan kita tulis di tempatkan di dalam layout tersebut.

//import layout admin
import LayoutAdmin from "../../../layouts/Admin";
<LayoutAdmin>

	//...
	
</LayoutAdmin>

Langkah 2 - Konfigurasi Private Route Place Edit

Setelah berhasil membuat view/component di atas, sekarang kita akan lanjutkan untuk membuat konfigurasi private route-nya.

Silahkan buka file src/routes/routes.jsx, kemudian ubah semua kode-nya menjadi seperti berikut ini :

//import react router dom
import { Routes, Route } from "react-router-dom";

//=======================================================================
//ADMIN
//=======================================================================

//import view Login
import Login from '../pages/admin/Login.jsx';

//import component private routes
import PrivateRoute from "./PrivateRoutes";

//import view admin Dashboard
import Dashboard from '../pages/admin/dashboard/Index.jsx';

//import view admin categories Index
import CategoriesIndex from '../pages/admin/categories/Index.jsx';

//import view admin category Create
import CategoryCreate from '../pages/admin/categories/Create.jsx';

//import view admin category Edit
import CategoryEdit from '../pages/admin/categories/Edit.jsx';

//import view admin places Index
import PlacesIndex from '../pages/admin/places/Index.jsx';

//import view admin places Create
import PlaceCreate from '../pages/admin/places/Create.jsx';

//import view admin places Edit
import PlaceEdit from '../pages/admin/places/Edit.jsx';

function RoutesIndex() {
    return (
        <Routes>

            {/* route "/admin/login" */}
            <Route path="/admin/login" element={<Login />} />

            {/* private route "/admin/dashboard" */}
            <Route
                path="/admin/dashboard"
                element={
                        <PrivateRoute>
                            <Dashboard />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories" */}
            <Route
                path="/admin/categories"
                element={
                        <PrivateRoute>
                            <CategoriesIndex />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories/create" */}
            <Route
                path="/admin/categories/create"
                element={
                        <PrivateRoute>
                            <CategoryCreate />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/categories/edit/:id" */}
            <Route
                path="/admin/categories/edit/:id"
                element={
                        <PrivateRoute>
                            <CategoryEdit />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/places" */}
            <Route
                path="/admin/places"
                element={
                        <PrivateRoute>
                            <PlacesIndex />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/places/create" */}
            <Route
                path="/admin/places/create"
                element={
                        <PrivateRoute>
                            <PlaceCreate />
                        </PrivateRoute>
                }
            />

            {/* private route "/admin/places/edit/:id" */}
            <Route
                path="/admin/places/edit/:id"
                element={
                        <PrivateRoute>
                            <PlaceEdit />
                        </PrivateRoute>
                }
            />

        </Routes>
    )
}

export default RoutesIndex

Dari perubahan kode di atas, pertama kita import view/component place edit terlebih dahulu.

//import view admin places Edit
import PlaceEdit from '../pages/admin/places/Edit.jsx';

Setelah itu, kita membuat konfigurasi untuk private route-nya, kurang lebih seperti berikut ini :

{/* private route "/admin/places/edit/:id" */}
<Route
    path="/admin/places/edit/:id"
    element={
    <PrivateRoute>
        <PlaceEdit />
    </PrivateRoute>
    }
/>

Di atas, untuk path atau URL dari route yang kita buat adalah /admin/places/edit/:id, dimana nilai :id akan bersifat dinamis dan berubah-ubah sesuai dengan data yang dikirimkan.

Membuat Proses Edit Data Place


Pada materi sebelumnya kita telah belajar bagaimana cara membuat konfigurasi private route untuk menampilkan halaman edit data place. Sebelum itu kita akan menaambahkan sebuah button edit di dalam halaman place index terlebih dahulu.

Langkah 1 - Menampilkan Button Edit

Sekarang kita akan melakukan sedikti perubahan untuk menambahkan button edit di halaman place index. Silahkan buka file src/pages/admin/places/Index.js, kemudian cari kode berikut ini :

<td className="text-center">
   <button onClick={() => deletePlace(place.id)} className="btn btn-sm btn-danger"><i className="fa fa-trash"></i></button>
</td>

Dan ubahlah menjadi seperti berikut ini :

<td className="text-center">
	<Link to={`/admin/places/edit/${place.id}`} className="btn btn-sm btn-primary me-2"><i className="fa fa-pencil-alt"></i></Link>
    <button onClick={() => deletePlace(place.id)} className="btn btn-sm btn-danger"><i className="fa fa-trash"></i></button>
</td>

Di atas kita menambahkan button baru dengan mengarahkan ke dalam URL /admin/places/edit/${place.id}, dimana akan menuju ke dalam halaman edit place berdasarkan ID dari data place.

Jika kita reload/refresh halaman place index, maka kita akan mendapatkan button baru untuk edit data. Kurang lebih seperti berikut ini :

Langkah 2 - Edit View/Component Place Edit

Sekarang kita lanjutkan untuk melakukan perubahan kode di dalam file place edit. Silahkan buka file src/pages/admin/places/Edit.jsx, kemudian ubah kode-nya menjadi seperti berikut ini :

//import hook from react
import React, { useState, useEffect } from "react";

//import layout
import LayoutAdmin from "../../../layouts/Admin";

//import BASE URL API
import Api from "../../../api";

//import hook navigate dari react router dom
import { useNavigate, useParams } from "react-router-dom";

//import js cookie
import Cookies from "js-cookie";

//import toats
import toast from "react-hot-toast";

//import react Quill
import ReactQuill from "react-quill";

// quill CSS
import 'react-quill/dist/quill.snow.css';

function PlaceEdit() {

    //title page
    document.title = "Edit Place - Administrator Travel GIS";

    //state form
    const [title, setTitle] = useState("");
    const [categoryID, setCategoryID] = useState("");
    const [description, setDescription] = useState("");
    const [phone, setPhone] = useState("");
    const [website, setWebsite] = useState("");
    const [office_hours, setOfficeHours] = useState("");
    const [address, setAddress] = useState("");
    const [latitude, setLatitude] = useState("");
    const [longitude, setLongitude] = useState("");

    //state image array / multiple
    const [images, setImages] = useState([]);

    //state categories
    const [categories, setCategories] = useState([]);

    //state validation
    const [validation, setValidation] = useState({});

    //token
    const token = Cookies.get("token");

    //navigate
    const navigate = useNavigate();

    //get ID from parameter URL
    const { id } = useParams();

    //function "fetchCategories"
    const fetchCategories = async () => {
        //fetching data from Rest API
        await Api.get("/api/web/categories")
            .then((response) => {
                //set data response to state "catgeories"
                setCategories(response.data.data);
            });
    };

    //function "getPlaceById"
    const getPlaceById = async () => {
        //fetching data from Rest API
        await Api.get(`/api/admin/places/${id}`, {
            headers: {
                //header Bearer + Token
                Authorization: `Bearer ${token}`,
            },
        }).then((response) => {
            //set data response to state
            setTitle(response.data.data.title);
            setCategoryID(response.data.data.category_id);
            setDescription(response.data.data.description);
            setPhone(response.data.data.phone);
            setWebsite(response.data.data.website);
            setOfficeHours(response.data.data.office_hours);
            setAddress(response.data.data.address);
            setLatitude(response.data.data.latitude);
            setLongitude(response.data.data.longitude);
        });
    };

    //hook
    useEffect(() => {
        //call function "fetchCategories"
        fetchCategories();

        //fetch function "getPlaceById"
        getPlaceById();

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    //function "handleFileChange"
    const handleFileChange = (e) => {
        
        //define variable for get value image data
        const imageData = e.target.files;

        Array.from(imageData).forEach(image => {
          //check validation file
          if(!image.type.match('image.*')) {

              setImages([]);

              //show toast
              toast.error("Format File not Supported!", {
              duration: 4000,
              position: "top-right",
              style: {
                  borderRadius: '10px',
                  background: '#333',
                  color: '#fff',
              },
              });

              return
          } else {
              setImages([...e.target.files]);
          }
        });
    }

    //function "updatePlace"
    const updatePlace = async (e) => {
        e.preventDefault();

        //define formData
        const formData = new FormData();

        //append data to "formData"
        formData.append("title", title);
        formData.append("category_id", categoryID);
        formData.append("description", description);
        formData.append("phone", phone);
        formData.append("website", website);
        formData.append("office_hours", office_hours);
        formData.append("address", address);
        formData.append("latitude", latitude);
        formData.append("longitude", longitude);
        formData.append("_method", "PATCH");

        Array.from(images).forEach(image => {
          formData.append("image[]", image);
        });

        await Api.post(`/api/admin/places/${id}`, formData, {
                //header
                headers: {
                    //header Bearer + Token
                    'Authorization': `Bearer ${token}`,
                    'content-type': 'multipart/form-data'
                },
            })
            .then(() => {
                //show toast
                toast.success("Data Updated Successfully!", {
                    duration: 4000,
                    position: "top-right",
                    style: {
                        borderRadius: "10px",
                        background: "#333",
                        color: "#fff",
                    },
                });

                //redirect place index page
                navigate("/admin/places");
            })
            .catch((error) => {
                //set state "validation"
                setValidation(error.response.data);
            });
    };

    return (
        <React.Fragment>
            <LayoutAdmin>
              <div className="row mt-4 mb-4">
                <div className="col-12">
                  <div className="card border-0 rounded shadow-sm border-top-success">
                    <div className="card-header">
                      <span className="font-weight-bold">
                        <i className="fa fa-map-marked-alt"></i> EDIT PLACE
                      </span>
                    </div>
                    <div className="card-body">
                      <form onSubmit={updatePlace}>
                        <div className="mb-3">
                          <label className="form-label fw-bold">
                            Image (<i>select many file</i>)
                          </label>
                          <input type="file" className="form-control" onChange={handleFileChange} multiple />
                        </div>
                        <div className="mb-3">
                          <label className="form-label fw-bold">Title</label>
                          <input type="text" className="form-control" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Enter Title Place" />
                        </div>
                        {validation.title && (
                          <div className="alert alert-danger">
                            {validation.title[0]}
                          </div>
                        )}
                        <div className="row">
                          <div className="col-md-6">
                            <div className="mb-3">
                              <label className="form-label fw-bold">
                                Category
                              </label>
                              <select class="form-select" value={categoryID} onChange={(e) => setCategoryID(e.target.value)}>
                                <option value="">-- Select Category --</option>
                                {categories.map((category) => (
                                  <option value={category.id} key={category.id}>
                                    {category.name}
                                  </option>
                                ))}
                              </select>
                            </div>
                            {validation.category_id && (
                              <div className="alert alert-danger">
                                {validation.category_id[0]}
                              </div>
                            )}
                          </div>
                          <div className="col-md-6">
                            <div className="mb-3">
                              <label className="form-label fw-bold">
                                Office Hours
                              </label>
                              <input type="text" className="form-control" value={office_hours} onChange={(e) => setOfficeHours(e.target.value)} placeholder="Enter Office Hours"
                              />
                            </div>
                            {validation.office_hours && (
                              <div className="alert alert-danger">
                                {validation.office_hours[0]}
                              </div>
                            )}
                          </div>
                        </div>
                        <div className="mb-3">
                          <label className="form-label fw-bold">
                            Description
                          </label>
                          <ReactQuill theme="snow" rows="5" value={description} onChange={(content) => setDescription(content)}
                          />
                        </div>
                        {validation.description && (
                          <div className="alert alert-danger">
                            {validation.description[0]}
                          </div>
                        )}
                        <div className="row">
                          <div className="col-md-6">
                            <div className="mb-3">
                              <label className="form-label fw-bold">
                                Phone
                              </label>
                              <input type="text" className="form-control" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="Enter Phone" />
                            </div>
                            {validation.phone && (
                              <div className="alert alert-danger">
                                {validation.phone[0]}
                              </div>
                            )}
                          </div>
                          <div className="col-md-6">
                            <div className="mb-3">
                              <label className="form-label fw-bold">
                                Website
                              </label>
                              <input type="text" className="form-control" value={website} onChange={(e) => setWebsite(e.target.value)} placeholder="Enter Website Place" />
                            </div>
                            {validation.website && (
                              <div className="alert alert-danger">
                                {validation.title[0]}
                              </div>
                            )}
                          </div>
                        </div>
                        <div className="mb-3">
                          <label className="form-label fw-bold">Address</label>
                          <textarea class="form-control" rows="3" value={address} onChange={(e) => setAddress(e.target.value)} placeholder="Enter Address Place"></textarea>
                        </div>
                        {validation.address && (
                          <div className="alert alert-danger">
                            {validation.address[0]}
                          </div>
                        )}
                        <div className="row">
                          <div className="col-md-6">
                            <div className="mb-3">
                              <label className="form-label fw-bold">
                                Latitude
                              </label>
                              <input type="text" className="form-control" value={latitude} onChange={(e) => setLatitude(e.target.value)} placeholder="Enter Latitude Place"/>
                            </div>
                            {validation.latitude && (
                              <div className="alert alert-danger">
                                {validation.latitude[0]}
                              </div>
                            )}
                          </div>
                          <div className="col-md-6">
                            <div className="mb-3">
                              <label className="form-label fw-bold">
                                Longitude
                              </label>
                              <input type="text" className="form-control" value={longitude} onChange={(e) => setLongitude(e.target.value)} placeholder="Enter Longitude Place" />
                            </div>
                            {validation.longitude && (
                              <div className="alert alert-danger">
                                {validation.longitude[0]}
                              </div>
                            )}
                          </div>
                        </div>
                        <div>
                          <button type="submit" className="btn btn-md btn-success me-2" >
                            <i className="fa fa-save"></i> UPDATE
                          </button>
                          <button type="reset" className="btn btn-md btn-warning">
                            <i className="fa fa-redo"></i> RESET
                          </button>
                        </div>
                      </form>
                    </div>
                  </div>
                </div>
              </div>
            </LayoutAdmin>
        </React.Fragment>
    );
}

export default PlaceEdit;

Dari perubahan kode di atas, pertama kita import hook dari react, yaitu useState dan useEffect.

//import hook from react
import React, { useState, useEffect } from "react";

Karena kita akan melakukan HTTP request, maka kita akan import konfigurasi endpoint yang sudah kita buat sebelumnya.

//import BASE URL API
import Api from "../../../api";

Setelah itu, kita import hook dari React Router DOM, yaitu useNavigate dan useParams. Untuk hook useNavigate kita gunakan untuk navigasi, sedangkan hook useParams untuk mendapatkan data dari parameter URL.

//import hook navigate dari react router dom
import { useNavigate, useParams } from "react-router-dom";

Kemudian kita import Js Cookie, karena kita akan gunakan untuk mendapatkan cookies di dalam browser dengan lebih mudah.

//import js cookie
import Cookies from "js-cookie";

Nanti, setelah proses update data berhasil kita akan menampilkan sebuah notifikasi dan untuk membuat notifikasi tersebut kita butuh package yang bernama React Hot Toast.

//import toats
import toast from "react-hot-toast";

Dan kita juga import React Quill, yang digunakan untuk menampilkan Text Editor atau biasa disebut dengan WYSIWYG.

//import react Quill
import ReactQuill from "react-quill";

// quill CSS
import 'react-quill/dist/quill.snow.css';

Kemudian di dalam function component PlaceEdit, pertama-tama kita mendefinisikan title untuk halaman ini.

//title page
document.title = "Edit Place - Administrator Travel GIS";

Setelah itu, kita membuat beberapa state yang akan kita gunakan untuk menyimpan data yang dikirimkan oleh form.

//state form
const [title, setTitle] = useState("");
const [categoryID, setCategoryID] = useState("");
const [description, setDescription] = useState("");
const [phone, setPhone] = useState("");
const [website, setWebsite] = useState("");
const [office_hours, setOfficeHours] = useState("");
const [address, setAddress] = useState("");
const [latitude, setLatitude] = useState("");
const [longitude, setLongitude] = useState("");

Kemudian buat state lagi untuk menyimpan data images dan categories dan state ini akan kita inisialisasi dengan jenis array, karena akan menyimpan data lebih dari satu.

//state image array / multiple
const [images, setImages] = useState([]);

//state categories
const [categories, setCategories] = useState([]);

Dan kita buat lagi state untuk menyimpan response validation dari Rest API dan untuk jenis datanya adalah object.

//state validation
const [validation, setValidation] = useState({});

Untuk mempermudah dalam menggunakan token yang ada di dalam cookies, maka kita buat variable baru dengan nama token yang berisi cookies dari browser.

//token
const token = Cookies.get("token");

Dan kita buat variable lagi dengan nama navigate, dimana isinya adalah hook useNavigate dari React Router DOM. Tujuannya untuk mempermudah kita dalam menggunakan hook tersebut.

//navigate
const navigate = e();

Setelah itu, kita buat variable dengan jenis object yang bernama id dan isinya adalah hook useParams dari React Router DOM. Variable ini akan digunakan untuk mengambil nilai ID yang ada di URL browser.

//get ID from parameter URL
const { id } = useParams();

kemudian kita membuat sebuah function yang bernama fetchCategories dengan jenis asynchronus. Fungsi ini akan digunakan untuk melakukan HTTP request ke dalam server untuk mendapatkan list data categories untuk ditampilkan di dalam form.

//function "fetchCategories"
const fetchCategories = async () => {

	//...
	
}

Di dalam function fetchCategories di atas, kita melakukan HTTP request ke dalam endpoint /api/web/categories dengan method GET.

//fetching data from Rest API
await Api.get("/api/web/categories")

Jika proses fetching tersebut berhasil, maka kita akan assign response datanya ke dalam state categories.

//set data response to state "catgeories"
setCategories(response.data.data);

Setelah itu, kita buat lagi function yang bernama getPlaceById. Fungsi ini digunakan untuk mendapatkan detail data place berdasarkan ID.

//function "getPlaceById"
const getPlaceById = async () => {

	//...
	
}

Di dalam function tersebut, kita melakukan HTTP request ke server dengan endpoint /api/admin/places/${id}. DImana nilai ID akan bersifat dinamis sesuai dengan data yang ada di dalam parameter URL browser.

//fetching data from Rest API
await Api.get(`/api/admin/places/${id}`, {
    headers: {
        //header Bearer + Token
        Authorization: `Bearer ${token}`,
    },
})

Jika proses fetching di atas berhasil, maka kita akan assign response datanya ke dalam beberapa state, yang mana nanti data tersebut akan ditampilkan di dalam form untuk di edit dan update.

//set data response to state
setTitle(response.data.data.title);
setCategoryID(response.data.data.category_id);
setDescription(response.data.data.description);
setPhone(response.data.data.phone);
setWebsite(response.data.data.website);
setOfficeHours(response.data.data.office_hours);
setAddress(response.data.data.address);
setLatitude(response.data.data.latitude);
setLongitude(response.data.data.longitude);

Dan agar function fetchCategories dan getPlaceById dapat dijalankan saat component diload, maka kita perlu memanggil kedua function tersebut di dalam hook useEffect.

//hook
useEffect(() => {
    //call function "fetchCategories"
    fetchCategories();

    //fetch function "getPlaceById"
    getPlaceById();

    // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Setelah itu, kita buat function yang bernama handleFileChange. Fungsi tersebut akan kita gunakan untuk memeriksa file gambar yang akan diupload, apakah sudah sesuai atau belum.

//function "handleFileChange"
const handleFileChange = (e) => {

	//...
	
}

Di dalam function tersebut, pertama kita buat variable dengan nama imageData yang berisin data gambar (multiple) yang dikirimkan melalui form.

//define variable for get value image data
const imageData = e.target.files;

Setelah itu, kita akan lakukan perulangan untuk memeriksa ektensi dari masing-masing file yang akan diupload.

Array.from(imageData).forEach(image => {

	//...

}

Jika ada salah satu file yang tidak memiliki ekstensi gambar, maka kita akan menampilkan notifikasi error menggukan React Hot Toast yang berisi informasi format file tidak disupport.

if (!image.type.match('image.*')) {

    setImages([]);

    //show toast
    toast.error("Format File not Supported!", {
        duration: 4000,
        position: "top-right",
        style: {
            borderRadius: '10px',
            background: '#333',
            color: '#fff',
        },
    });

    return
}

Tapi jika file yang akan diupload sudah sesuai, maka kita akan assign ke dalam state images menggunakan Spread Operator.

setImages([...e.target.files]);

Setelah itu, kita buat function lagi yang bernama updatePlace. Fungsi tersebut kita gunakan untuk mengupdate data place ke dalam server. Function tersebut akan dijalankan ketika form disubmit.

<form onSubmit={updatePlace}>

	//...
	
</form>
//function "updatePlace"
const updatePlace = async (e) => {

	//...
	
}

Di dalam function updatePlace, pertama-tama kita melakukan inisialisasi formData terlebih dahulu.

//define formData
const formData = new FormData();

Kemudian kita membuat append formData yang berisi key dan value yang kita ambil dari state yang sudah kita buat sebelumnya. Tujuannya untuk mengempokkan data agar lebih mudah dikirimkan ke dalam server.

//append data to "formData"
formData.append("title", title);
formData.append("category_id", categoryID);
formData.append("description", description);
formData.append("phone", phone);
formData.append("website", website);
formData.append("office_hours", office_hours);
formData.append("address", address);
formData.append("latitude", latitude);
formData.append("longitude", longitude);
formData.append("_method", "PATCH");

Array.from(images).forEach(image => {
    formData.append("image[]", image);
});

Setelah itu, kita kirim formData tersebut ke server dengan Rest API ke dalam endpoint /api/admin/places/${id} menggunakan method POST.

await Api.post(`/api/admin/places/${id}`, formData, {
    //header
    headers: {
        //header Bearer + Token
        'Authorization': `Bearer ${token}`,
        'content-type': 'multipart/form-data'
    },
})

Jika proses update data berhasil dilakukan di dalam server, maka kita akan menampilkan notifikasi menggunakan React Hot Toast yang berisi informasi data berhasil diupdate.

//show toast
toast.success("Data Updated Successfully!", {
    duration: 4000,
    position: "top-right",
    style: {
        borderRadius: "10px",
        background: "#333",
        color: "#fff",
    },
});

Setelah itu, kita arahkan ke dalam halaman place index menggunakan hook useNavigate.

//redirect place index page
navigate("/admin/places");

Jika proses update gagal dilakukan, maka akan melakukan assign response error validasi ke dalam state validation.

//set state "validation"
setValidation(error.response.data);

Langkah 3 - Uji Coba Proses Edit dan Update Place

Sekarang silahkan klik button edit di salah satu data place. Jika berhasil maka kita akan menampilkan halaman edit place seperti berikut ini :

Di atas silahkan sesuaikan data yang ingin diupdate, jika sudah silahkan klik button UPDATE dan jika berhasil maka akan di arahkan ke halaman place index dengan data yang telah diperbarui.

Menambahkan Mapbox Geocoder di Halaman Edit Data Place


Dimateri sebelumnya kita telah berhasil membut fitur edit dan update data place, namun kita masih belum menampilkan sebuah maps untuk mempermudah kita dalam mengubah nilai longitude dan latitude. Oleh sebab itu, dimateri kali ini kita akan belajar menambahkan Mapbox dan Geocoder di dalam halaman edit data place.

Langkah 1 - Edit View/Component Place Edit

Sekarang kita akan lanjutkan untuk melakukan penambahan kode di dalam view/component place edit. Dimana kita akan menambahkan konfigurasi Mapbox dan Geocoder untuk menampilkan sebuah maps beserta kolom pencarian di dalam maps tersebut.

Silahkan buka file src/pages/admin/places/Edit.jsx, kemudian ubah semua kode yang ada menjadi seperti berikut ini :

//import hook from react
import React, { useState, useEffect, useRef } from "react";

//import layout
import LayoutAdmin from "../../../layouts/Admin";

//import BASE URL API
import Api from "../../../api";

//import hook navigate dari react router dom
import { useNavigate, useParams } from "react-router-dom";

//import js cookie
import Cookies from "js-cookie";

//import toats
import toast from "react-hot-toast";

//import react Quill
import ReactQuill from "react-quill";

// quill CSS
import 'react-quill/dist/quill.snow.css';

//mapbox gl
import mapboxgl from 'mapbox-gl'; // eslint-disable-line import/no-webpack-loader-syntax

//mapbox gl geocoder
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';

//api key mapbox
mapboxgl.accessToken = import.meta.env.VITE_APP_MAPBOX;

function PlaceEdit() {

    //title page
    document.title = "Edit Place - Administrator Travel GIS";

    //state form
    const [title, setTitle] = useState("");
    const [categoryID, setCategoryID] = useState("");
    const [description, setDescription] = useState("");
    const [phone, setPhone] = useState("");
    const [website, setWebsite] = useState("");
    const [office_hours, setOfficeHours] = useState("");
    const [address, setAddress] = useState("");
    const [latitude, setLatitude] = useState("");
    const [longitude, setLongitude] = useState("");

    //state image array / multiple
    const [images, setImages] = useState([]);

    //state categories
    const [categories, setCategories] = useState([]);

    //state validation
    const [validation, setValidation] = useState({});

    //token
    const token = Cookies.get("token");

    //navigate
    const navigate = useNavigate();

    //get ID from parameter URL
    const { id } = useParams();

    //function "fetchCategories"
    const fetchCategories = async () => {
        //fetching data from Rest API
        await Api.get("/api/web/categories")
            .then((response) => {
                //set data response to state "catgeories"
                setCategories(response.data.data);
            });
    };

    //function "getPlaceById"
    const getPlaceById = async () => {
        //fetching data from Rest API
        await Api.get(`/api/admin/places/${id}`, {
            headers: {
                //header Bearer + Token
                Authorization: `Bearer ${token}`,
            },
        }).then((response) => {
            //set data response to state
            setTitle(response.data.data.title);
            setCategoryID(response.data.data.category_id);
            setDescription(response.data.data.description);
            setPhone(response.data.data.phone);
            setWebsite(response.data.data.website);
            setOfficeHours(response.data.data.office_hours);
            setAddress(response.data.data.address);
            setLatitude(response.data.data.latitude);
            setLongitude(response.data.data.longitude);
        });
    };

    //hook
    useEffect(() => {
        //call function "fetchCategories"
        fetchCategories();

        //fetch function "getPlaceById"
        getPlaceById();

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    //function "handleFileChange"
    const handleFileChange = (e) => {
        
        //define variable for get value image data
        const imageData = e.target.files;

        Array.from(imageData).forEach(image => {
          //check validation file
          if(!image.type.match('image.*')) {

              setImages([]);

              //show toast
              toast.error("Format File not Supported!", {
              duration: 4000,
              position: "top-right",
              style: {
                  borderRadius: '10px',
                  background: '#333',
                  color: '#fff',
              },
              });

              return
          } else {
              setImages([...e.target.files]);
          }
        });
    }

    //function "updatePlace"
    const updatePlace = async (e) => {
        e.preventDefault();

        //define formData
        const formData = new FormData();

        //append data to "formData"
        formData.append("title", title);
        formData.append("category_id", categoryID);
        formData.append("description", description);
        formData.append("phone", phone);
        formData.append("website", website);
        formData.append("office_hours", office_hours);
        formData.append("address", address);
        formData.append("latitude", latitude);
        formData.append("longitude", longitude);
        formData.append("_method", "PATCH");

        Array.from(images).forEach(image => {
          formData.append("image[]", image);
        });

        await Api.post(`/api/admin/places/${id}`, formData, {
                //header
                headers: {
                    //header Bearer + Token
                    'Authorization': `Bearer ${token}`,
                    'content-type': 'multipart/form-data'
                },
            })
            .then(() => {
                //show toast
                toast.success("Data Updated Successfully!", {
                    duration: 4000,
                    position: "top-right",
                    style: {
                        borderRadius: "10px",
                        background: "#333",
                        color: "#fff",
                    },
                });

                //redirect place index page
                navigate("/admin/places");
            })
            .catch((error) => {
                //set state "validation"
                setValidation(error.response.data);
            });
    };

    //=========================================================
    //MAPBOX
    //=========================================================

    //define state
    const mapContainer = useRef(null);

    useEffect(() => {
        //init map
        const map = new mapboxgl.Map({
            container: mapContainer.current,
            style: 'mapbox://styles/mapbox/streets-v12',
            center: [longitude, latitude],
            zoom: 15,
        });

        //init geocoder
        const geocoder = new MapboxGeocoder({
            accessToken: mapboxgl.accessToken,

            marker: {
                draggable: true,
            },

            mapboxgl: mapboxgl,
        });

        map.addControl(geocoder);

        //init marker
        const marker = new mapboxgl.Marker({
                draggable: true,
                color: "rgb(47 128 237)",
            })
            .setLngLat([longitude, latitude])
            .addTo(map);

        //geocoder result
        geocoder.on("result", function(e) {

            marker.remove();

            marker.setLngLat(e.result.center).addTo(map);

            marker.on("dragend", function(e) {
                setLatitude(e.target._lngLat.lat);
                setLongitude(e.target._lngLat.lng);
            });
        });
    });

    return (
        <React.Fragment>
            <LayoutAdmin>
              <div className="row mt-4 mb-4">
                <div className="col-12">
                  <div className="card border-0 rounded shadow-sm border-top-success">
                    <div className="card-header">
                      <span className="font-weight-bold">
                        <i className="fa fa-map-marked-alt"></i> EDIT PLACE
                      </span>
                    </div>
                    <div className="card-body">
                      <form onSubmit={updatePlace}>
                        <div className="mb-3">
                          <label className="form-label fw-bold">
                            Image (<i>select many file</i>)
                          </label>
                          <input type="file" className="form-control" onChange={handleFileChange} multiple />
                        </div>
                        <div className="mb-3">
                          <label className="form-label fw-bold">Title</label>
                          <input type="text" className="form-control" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Enter Title Place" />
                        </div>
                        {validation.title && (
                          <div className="alert alert-danger">
                            {validation.title[0]}
                          </div>
                        )}
                        <div className="row">
                          <div className="col-md-6">
                            <div className="mb-3">
                              <label className="form-label fw-bold">
                                Category
                              </label>
                              <select class="form-select" value={categoryID} onChange={(e) => setCategoryID(e.target.value)}>
                                <option value="">-- Select Category --</option>
                                {categories.map((category) => (
                                  <option value={category.id} key={category.id}>
                                    {category.name}
                                  </option>
                                ))}
                              </select>
                            </div>
                            {validation.category_id && (
                              <div className="alert alert-danger">
                                {validation.category_id[0]}
                              </div>
                            )}
                          </div>
                          <div className="col-md-6">
                            <div className="mb-3">
                              <label className="form-label fw-bold">
                                Office Hours
                              </label>
                              <input type="text" className="form-control" value={office_hours} onChange={(e) => setOfficeHours(e.target.value)} placeholder="Enter Office Hours"
                              />
                            </div>
                            {validation.office_hours && (
                              <div className="alert alert-danger">
                                {validation.office_hours[0]}
                              </div>
                            )}
                          </div>
                        </div>
                        <div className="mb-3">
                          <label className="form-label fw-bold">
                            Description
                          </label>
                          <ReactQuill theme="snow" rows="5" value={description} onChange={(content) => setDescription(content)}
                          />
                        </div>
                        {validation.description && (
                          <div className="alert alert-danger">
                            {validation.description[0]}
                          </div>
                        )}
                        <div className="row">
                          <div className="col-md-6">
                            <div className="mb-3">
                              <label className="form-label fw-bold">
                                Phone
                              </label>
                              <input type="text" className="form-control" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="Enter Phone" />
                            </div>
                            {validation.phone && (
                              <div className="alert alert-danger">
                                {validation.phone[0]}
                              </div>
                            )}
                          </div>
                          <div className="col-md-6">
                            <div className="mb-3">
                              <label className="form-label fw-bold">
                                Website
                              </label>
                              <input type="text" className="form-control" value={website} onChange={(e) => setWebsite(e.target.value)} placeholder="Enter Website Place" />
                            </div>
                            {validation.website && (
                              <div className="alert alert-danger">
                                {validation.title[0]}
                              </div>
                            )}
                          </div>
                        </div>
                        <div className="mb-3">
                          <label className="form-label fw-bold">Address</label>
                          <textarea class="form-control" rows="3" value={address} onChange={(e) => setAddress(e.target.value)} placeholder="Enter Address Place"></textarea>
                        </div>
                        {validation.address && (
                          <div className="alert alert-danger">
                            {validation.address[0]}
                          </div>
                        )}
                        <div className="row">
                          <div className="col-md-6">
                            <div className="mb-3">
                              <label className="form-label fw-bold">
                                Latitude
                              </label>
                              <input type="text" className="form-control" value={latitude} onChange={(e) => setLatitude(e.target.value)} placeholder="Enter Latitude Place"/>
                            </div>
                            {validation.latitude && (
                              <div className="alert alert-danger">
                                {validation.latitude[0]}
                              </div>
                            )}
                          </div>
                          <div className="col-md-6">
                            <div className="mb-3">
                              <label className="form-label fw-bold">
                                Longitude
                              </label>
                              <input type="text" className="form-control" value={longitude} onChange={(e) => setLongitude(e.target.value)} placeholder="Enter Longitude Place" />
                            </div>
                            {validation.longitude && (
                              <div className="alert alert-danger">
                                {validation.longitude[0]}
                              </div>
                            )}
                          </div>
                        </div>
                        <div className="row mb-3">
                            <div className="col-md-12">
                                <div ref={mapContainer} className="map-container" />
                            </div>
                        </div>
                        <div>
                          <button type="submit" className="btn btn-md btn-success me-2" >
                            <i className="fa fa-save"></i> UPDATE
                          </button>
                          <button type="reset" className="btn btn-md btn-warning">
                            <i className="fa fa-redo"></i> RESET
                          </button>
                        </div>
                      </form>
                    </div>
                  </div>
                </div>
              </div>
            </LayoutAdmin>
        </React.Fragment>
    );
}

export default PlaceEdit;

Dari penambahan kode di atas, pertama kita import hook useRef dari React.

//import hook from react
import React, { useState, useEffect, useRef } from "react";

Setelah itu, kita import package Mapbox GL dan Mapbox Geocoder. Untuk Mapbox GL akan kita gunakan menampilkan maps atau peta, sedangkan Geocoder akan digunakan untuk menambahkan pencarian di dalam maps.

//mapbox gl
import mapboxgl from 'mapbox-gl'; // eslint-disable-line import/no-webpack-loader-syntax

//mapbox gl geocoder
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';

Kemudian kita konfigurasi access token untuk Mapbox dari file .env.

//api key mapbox
mapboxgl.accessToken = import.meta.env.VITE_APP_MAPBOX;

Dan di dalam function component, pertama-tama kita define variable dengan nama mapContainer dan variable tersebut menggunakan hook useRef. Nantinya akan kita gunakan untuk melakukan render maps di dalam attribute HTML.

//define state
const mapContainer = useRef(null);

Setelah itu, agar maps dapat dijalankan pertama kali saat view/component diload, maka kita perlu menaaruhnya di dalam hook useEffect.

useEffect(() => {

	//...
	
}

Di dalam hook useEffect, pertama kita melakukan inisialisasi maps dengan Mapbox.

//init map
const map = new mapboxgl.Map({
    container: mapContainer.current,
    style: 'mapbox://styles/mapbox/streets-v12',
    center: [longitude, latitude],
    zoom: 15,
});

Setelah itu, kita lanjutkan untuk melakukan inisialisasi Geocoder untuk pencarian di dalam maps.

//init geocoder
const geocoder = new MapboxGeocoder({
    accessToken: mapboxgl.accessToken,

    marker: {
        draggable: true,
    },

    mapboxgl: mapboxgl,
});

Setelah berhasil melakukan inisialisasi, selanjutnya kita akan gunakan Geocoder di dalam Mapbox dengan method addControl. Artinya kita akan menambahkan plugin Geocoder di dalam maps.

map.addControl(geocoder);

Kita lanjutkan lagi untuk melakukan inisialisasi marker atau pointer yang ada di dalam maps.

//init marker
const marker = new mapboxgl.Marker({
  draggable: true,
  color: "rgb(47 128 237)",
})
.setLngLat([longitude, latitude])
.addTo(map);

Kemudian kita membaca event yang bernama result dari Geocoder. Event ini akan dijalankan ketika kita selesai melakukan pencarian data di dalam maps.

//geocoder result
geocoder.on("result", function(e) {

	//...
	
}

Di dalam event result, pertama-tama kita akan menghapus marker yang lama.

marker.remove();

Setelah itu, kita akan ganti dengan marker yang baru dengan melakukan assign longitude dan latitude dari hasil pencarian di dalam maps.

marker.setLngLat(e.result.center).addTo(map);

Kemudian kita membaca event lagi yang bernama dragend dari marker. Event ini akan dijalankan ketika kita selesai melakukan drag / menggeser sebuah marker.

marker.on("dragend", function(e) {

	//...
	
}

Di dalam event dragend tersebut, kita akan melakukan assign nilai longitude dan latitude ke dalam state yang di dapatkan saat melakukan drag marker.

setLatitude(e.target._lngLat.lat);
setLongitude(e.target._lngLat.lng);

Langkah 2 - Uji Coba Maps dan Geocoder

Sekarang silahkan klik button edit di dalam halaman place index dan jika berhasil maka akan menampilkan sebuah maps dan kolom pencarian di dalam halaman edit data place.

Beranda Mundur Maju