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 12: Implementasi Halaman Pengguna #3

Menampilkan Gambar Slider di Halaman Detail Place


Dimateri sebelumnya kita telah berhasil menampilkan detail data place, seperti title, description, address dan lain-lain. Tetapi masih ada yang kurang, yaitu menampilkan informasi gambar yang terkait dengan data place tersebut. Dan karena gambar yang dimiliki data place bisa lebih dari satu, maka untuk menampilkannya kita butuh menggunakan perulangan.

Dan untuk menampilkan gambar lebih dari satu, maka kita akan tampilkan dalam bentuk slider. Dengan tujuan hasil yang ditampilkan lebih interaktif dan bagus. Dan kita akan menggunakan package tambahan yang sudah pernah kita install di langkah-langkah awal. Jadi kita tinggal menggunakannya saja di dalam view/component ini.

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

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

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

//import layout web
import LayoutWeb from "../../../layouts/Web";

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

//import imageGallery
import ImageGallery from "react-image-gallery";

//import imageGallery CSS
import "react-image-gallery/styles/css/image-gallery.css";

function WebPlaceShow() {

    //state place
    const [place, setPlace] = useState({});

    //slug params
    const { slug } = useParams();

    //function "fetchDataPlace"
    const fetchDataPlace = async () => {
        //fetching Rest API
        await Api.get(`/api/web/places/${slug}`)
        .then((response) => {

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

            //set title from state "category"
            document.title = `${response.data.data.title} - Website Wisata Berbasis GIS (Geographic Information System)`;
        });
    };

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

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

    //=================================================================
    // react image gallery
    //=================================================================

    //define image array
    const images = [];

    //function "placeImages"
    const placeImages = () => {
        //loop data from object "place"
        for (let value in place.images) {
            //push to image array
            images.push({
                original: place.images[value].image,
                thumbnail: place.images[value].image,
            });
        }
    };

    //hook 
    useEffect(() => {

        //call function "placeImage"
        placeImages();
    });

    return (
        <React.Fragment>
          <LayoutWeb>
            <div className="container mt-80">
              <div className="row">
                <div className="col-md-7 mb-4">
                  <div className="card border-0 rounded shadow-sm">
                    <div className="card-body">
                      <h4>{place.title}</h4>
                      <span className="card-text">
                        <i className="fa fa-map-marker"></i> <i>{place.address}</i>
                      </span>
                      <hr />
                      <ImageGallery items={images} autoPlay={true} />
                      <div
                        dangerouslySetInnerHTML={{ __html: place.description }}
                      />
                    </div>
                  </div>
                </div>
                <div className="col-md-5 mb-4">
                  <div className="card border-0 rounded shadow-sm">
                    <div className="card-body">
                      <h5>
                        <i className="fa fa-map-marked-alt"></i> MAPS
                      </h5>
                      <hr />
                    </div>
                    <hr />
                    <div className="card-body">
                      <div className="row">
                        <div className="col-md-2 col-2">
                          <div className="icon-info-green">
                            <i className="fa fa-map-marker-alt"></i>
                          </div>
                        </div>
                        <div className="col-md-10 col-10">
                            <div className="capt-info fw-bold">ADDRESS</div>
                            <div className="sub-title-info"><i>{place.address}</i></div>
                        </div>
                        <div className="col-md-2 col-2">
                          <div className="icon-info-green">
                            <i className="fa fa-clock"></i>
                          </div>
                        </div>
                        <div className="col-md-10 col-10">
                          <div className="capt-info fw-bold">OFFICE HOURS</div>
                          <div className="sub-title-info">{place.office_hours}</div>
                        </div>
                        <div className="col-md-2 col-2">
                          <div className="icon-info-green">
                            <i className="fa fa-phone"></i>
                          </div>
                        </div>
                        <div className="col-md-10 col-10">
                          <div className="capt-info fw-bold">PHONE</div>
                          <div className="sub-title-info">{place.phone}</div>
                        </div>
                        <div className="col-md-2 col-2">
                          <div className="icon-info-green">
                            <i className="fa fa-globe-asia"></i>
                          </div>
                        </div>
                        <div className="col-md-10 col-10">
                          <div className="capt-info fw-bold">WEBSITE</div>
                          <div className="sub-title-info">
                            <a href={place.website} className="text-decoration-none">{place.website}</a>
                          </div>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </LayoutWeb>
        </React.Fragment>
    );
}

export default WebPlaceShow;

Dari perubahan kode di atas, pertama-tama kita import packgae React Image Gallery beserta file css-nya.

//import imageGallery
import ImageGallery from "react-image-gallery";

//import imageGallery CSS
import "react-image-gallery/styles/css/image-gallery.css";

Setelah itu, kita membuat sebuah variable baru yang bernama images menggunakan jenis array. Variable ini akan kita gunakan untuk menyimpan data gambar yang di dapatkan dari data place.

//define image array
const images = [];

Kemudian kita membuat function baru yang bernama placeImage. Function ini di dalamnya melakukan looping data images yang di dapatkan dari state place dan kita lakukan push ke dalam variable images di atas.

//function "placeImages"
const placeImages = () => {
    //loop data from object "place"
    for (let value in place.images) {
        //push to image array
        images.push({
            original: place.images[value].image,
            thumbnail: place.images[value].image,
        });
    }
};

Setelah itu, kita panggil function placeImages tersebut di dalam hook useEffect.

//hook 
useEffect(() => {

    //call function "placeImage"
    placeImages();
});

Dan untuk menampilkannya didalam JSX, kita tinggal panggil component <ImageGallery /> dengan menambahkan props berupa variable images.

<ImageGallery items={images} autoPlay={true} />

Sekarang, jika kita reload halaman detail data place, maka kita akan mendapatkan hasil yang kurang lebih seperti berikut ini :

Menampilkan Maps di Halaman Detail Data Palce


Setelah berhasil menampilkan informasi data place dan juga gambar yang terkait dengan data place, maka kita akan lanjutkan belajar bagaimana cara menampilkan titik lokasi data place tersebut di dalam maps.

Dimana kita akan menggunakan Mapbox untuk menampilkan data maps tersebut dan tidak hanya menampilkan titik lokasi, kita juga akan menambahkan nama place dan alamat di dalam maps tersebut.

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

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

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

//import layout web
import LayoutWeb from "../../../layouts/Web";

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

//import imageGallery
import ImageGallery from "react-image-gallery";

//import imageGallery CSS
import "react-image-gallery/styles/css/image-gallery.css";

//import mapbox gl
import mapboxgl from "mapbox-gl"; // eslint-disable-line import/no-webpack-loader-syntax

//api key mapbox
mapboxgl.accessToken = import.meta.env.VITE_APP_MAPBOX;

function WebPlaceShow() {

    //state place
    const [place, setPlace] = useState({});

    //map container
    const mapContainer = useRef(null);

    //slug params
    const { slug } = useParams();

    //function "fetchDataPlace"
    const fetchDataPlace = async () => {
        //fetching Rest API
        await Api.get(`/api/web/places/${slug}`)
        .then((response) => {

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

            //set title from state "category"
            document.title = `${response.data.data.title} - Website Wisata Berbasis GIS (Geographic Information System)`;
        });
    };

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

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

    //=================================================================
    // react image gallery
    //=================================================================

    //define image array
    const images = [];

    //function "placeImages"
    const placeImages = () => {
        //loop data from object "place"
        for (let value in place.images) {
            //push to image array
            images.push({
                original: place.images[value].image,
                thumbnail: place.images[value].image,
            });
        }
    };

    //=================================================================
    // mapbox
    //=================================================================

    //function "initMap"
    const initMap = () => {
        //init Map
        const map = new mapboxgl.Map({
            container: mapContainer.current,
            style: 'mapbox://styles/mapbox/streets-v12',
            center: [
                place.longitude ? place.longitude : "",
                place.latitude ? place.latitude : "",
            ],
            zoom: 15,
        });

        //init popup
        new mapboxgl.Popup({
                closeOnClick: false
            })
            .setLngLat([
                place.longitude ? place.longitude : "",
                place.latitude ? place.latitude : "",
            ])
            .setHTML(`<h6>${place.title}</h6><hr/><p><i>${place.address}</i></p>`)
            .addTo(map);
    };

    //hook 
    useEffect(() => {

        //call function "placeImage"
        placeImages();

        //call function "initMap"
        initMap();
    });

    return (
        <React.Fragment>
          <LayoutWeb>
            <div className="container mt-80">
              <div className="row">
                <div className="col-md-7 mb-4">
                  <div className="card border-0 rounded shadow-sm">
                    <div className="card-body">
                      <h4>{place.title}</h4>
                      <span className="card-text">
                        <i className="fa fa-map-marker"></i> <i>{place.address}</i>
                      </span>
                      <hr />
                      <ImageGallery items={images} />
                      <div
                        dangerouslySetInnerHTML={{ __html: place.description }}
                      />
                    </div>
                  </div>
                </div>
                <div className="col-md-5 mb-4">
                  <div className="card border-0 rounded shadow-sm">
                    <div className="card-body">
                      <h5>
                        <i className="fa fa-map-marked-alt"></i> MAPS
                      </h5>
                      <hr />
                      <div ref={mapContainer} className="map-container" style={{ height: "350px" }}/>
                    </div>
                    <hr />
                    <div className="card-body">
                      <div className="row">
                        <div className="col-md-2 col-2">
                          <div className="icon-info-green">
                            <i className="fa fa-map-marker-alt"></i>
                          </div>
                        </div>
                        <div className="col-md-10 col-10">
                            <div className="capt-info fw-bold">ADDRESS</div>
                            <div className="sub-title-info"><i>{place.address}</i></div>
                        </div>
                        <div className="col-md-2 col-2">
                          <div className="icon-info-green">
                            <i className="fa fa-clock"></i>
                          </div>
                        </div>
                        <div className="col-md-10 col-10">
                          <div className="capt-info fw-bold">OFFICE HOURS</div>
                          <div className="sub-title-info">{place.office_hours}</div>
                        </div>
                        <div className="col-md-2 col-2">
                          <div className="icon-info-green">
                            <i className="fa fa-phone"></i>
                          </div>
                        </div>
                        <div className="col-md-10 col-10">
                          <div className="capt-info fw-bold">PHONE</div>
                          <div className="sub-title-info">{place.phone}</div>
                        </div>
                        <div className="col-md-2 col-2">
                          <div className="icon-info-green">
                            <i className="fa fa-globe-asia"></i>
                          </div>
                        </div>
                        <div className="col-md-10 col-10">
                          <div className="capt-info fw-bold">WEBSITE</div>
                          <div className="sub-title-info">
                            <a href={place.website} className="text-decoration-none">{place.website}</a>
                          </div>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </LayoutWeb>
        </React.Fragment>
    );
}

export default WebPlaceShow;

Dari perubahan kode di atas, pertama kita import hook useRef dari react.

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

Setelah itu, kita import package MapboxGL dan konfigurasi access token dari file .env.

//import mapbox gl
import mapboxgl from "mapbox-gl"; // eslint-disable-line import/no-webpack-loader-syntax

//api key mapbox
mapboxgl.accessToken = import.meta.env.VITE_APP_MAPBOX;

Kemudian kita inisialisasi sebuah variable baru yang bernama mapContainer dengan isi hook dari useRef. Ini nantinya akan kita gunakan untuk menampilkan maps di dalam element HTML.

//map container
const mapContainer = useRef(null);

Setelah itu kita buat function baru yang bernama initMap, di dimana di dalamnya kita melakukan inisialisasi MapboxGL dengan beberapa object. Seperti container, style, center dan zoom.

//init Map
const map = new mapboxgl.Map({
    container: mapContainer.current,
    style: 'mapbox://styles/mapbox/streets-v12',
    center: [
        place.longitude ? place.longitude : "",
        place.latitude ? place.latitude : "",
    ],
    zoom: 15,
});

Setelah berhasil melakukan inisialisasi MapboxGL, selanjutnya kita melakukan inisialisasi untuk popup, dimana popup ini akan muncul di dalam peta dengan membawa informasi sesuai dengan yang kita berikan.

//init popup
new mapboxgl.Popup({
        closeOnClick: false
    })
    .setLngLat([
        place.longitude ? place.longitude : "",
        place.latitude ? place.latitude : "",
    ])
    .setHTML(`<h6>${place.title}</h6><hr/><p><i>${place.address}</i></p>`)
    .addTo(map);

Di atas, untuk longitude dan latitude tetap kita ambilkan dari state place.

less
.setLngLat([ place.longitude ? place.longitude : "", place.latitude ? place.latitude : "", ])

kemudian kita tampilkan popup tersebut dengan memberikan informasi title dan address dari data place yang sedang dibuka. Sebenarnya kita juga bisa memberikan informasi apapun disini, termasuk foto dari data place tersebut.

.setHTML(`<h6>${place.title}</h6><hr/><p><i>${place.address}</i></p>`)
.addTo(map);

Dan untuk menampilkan maps di dalam HTML, kita cukup menggunakan kode seperti berikut ini :

<div ref={mapContainer} className="map-container" style={{ height: "350px" }}/>

Sekarang, jika halaman detail place kita reload / refresh, maka hasilnya kurang lebih seperti berikut ini :

Konfigurasi Route Maps Directions


Sekarang kita lanjutkan belajar membuat route yang nantinya akan digunakan untuk menampilkan halaman direction dari maps. Direction digunakan untuk menampilkan sebuah rute dari dua titik lokasi, yaitu posisi kita sekarang dengan lokasi tujuan.

Langkah 1 - Membuat View/Component Direction

Seperti sebelumnya, kita harus membuat sebuah file view/component terlebih dahulu sebelum kita membuat konfigurasi route-nya. Silahkan buat file baru dengan nama Direction.jsx di dalam folder src/pages/web/place, kemudian masukkan kode berikut ini di dalamnya.

//import react
import React from "react";

//import layout web
import LayoutWeb from "../../../layouts/Web";

function WebPlacesDirection() {

    return(
        <React.Fragment>
            <LayoutWeb>
                <div className="container mt-80">
                    Halaman Places Direction
                </div>
            </LayoutWeb>
        </React.Fragment>
    )

}

export default WebPlacesDirection;

Dari penambahan kode di atas, kita hanya memberikan sample kode saja, karena kita akan ubah lagi di materi-materi selanjutnya sesuai dengan data yang ditampilkan.

Langkah 2 - Konfigurasi Route Map Direction

Setelah berhasil membuat view/component untuk halaman map direction, sekarang kita lanjutkan untuk membuat konfigurasi 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';

//import view admin sliders Index
import SlidersIndex from '../pages/admin/sliders/Index.jsx';

//import view admin slider Create
import SliderCreate from '../pages/admin/sliders/Create.jsx';

//import view admin users Index
import UsersIndex from '../pages/admin/users/Index.jsx';

//import view admin user Create
import UserCreate from '../pages/admin/users/Create.jsx';

//import view admin user Edit
import UserEdit from '../pages/admin/users/Edit.jsx';

//=======================================================================
//WEB
//=======================================================================

//import view web Home
import Home from '../pages/web/home/Index.jsx';

//import view web category Show
import WebCategoryShow from '../pages/web/categories/Show.jsx';

//import view web place Index
import WebPlacesIndex from "../pages/web/places/Index.jsx";

//import view web place Show
import WebPlaceShow from '../pages/web/places/Show.jsx';

//import view web place Direction
import WebPlaceDirection from '../pages/web/places/Direction.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>
                }
            />

            {/* private route "/admin/sliders" */}
            <Route
                path="/admin/sliders"
                element={
                        <PrivateRoute>
                            <SlidersIndex />
                        </PrivateRoute>
                }
            />

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

            {/* private route "/admin/users" */}
            <Route
                path="/admin/users"
                element={
                        <PrivateRoute>
                            <UsersIndex />
                        </PrivateRoute>
                }
            />

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

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

            {/* route "/" */}
            <Route path="/" element={<Home />} />

            {/* route "/category/:slug" */}
            <Route path="/category/:slug" element={<WebCategoryShow />} />

            {/* route "/places" */}
            <Route path="/places" element={<WebPlacesIndex />} />

            {/* route "/places/:slug" */}
            <Route path="/places/:slug" element={<WebPlaceShow />} />

            {/* route "/places/:slug/direction" */}
            <Route path="/places/:slug/direction" element={<WebPlaceDirection />} />

        </Routes>
    )
}

export default RoutesIndex

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

//import view web place Direction
import WebPlaceDirection from '../pages/web/places/Direction.jsx';

Setelah itu, kita buat konfigurasi route-nya dengan path /places/:slug/direction. Jika URL tersebut diakses, maka akan memanggil view/component yang bernama <WebPlaceDirection />.

Menampilkan Directions dari Lokasi Sekarang ke Titik Lokasi Maps


Sekarang kita akan belajar menampilkan sebuah directions di maps, yaitu menampilkan rute jalan dari lokasi kita (current) menuju titik tujuan (place).

Dimana untuk mendapatkan lokasi saat ini kita harus mengaktifkan GPS di dalam web browser, dan kita bisa membuatnya nanti menggunakan GeoLocation dari Mapbox.

INFORMASI : GPS yang aktif di dalam komputer (web browser) kurang akurat tapi sudah lumayan bagus.

Langkah 1 - Menambahkan Button Open Direction

Sekarang kita akan menambahkan button di dalam detail place, dimana button tersebut jika kita klik akan menuju ke dalam halaman map direction dengan membawa parameter data berupa longitude dan latitude melalui URL browser.

Silahkan buka file src/pages/web/places/Show.jsx, kemudian cari kode berikut ini :

<div ref={mapContainer} className="map-container" style={{ height: "350px" }}/>

Dan ubahlah menjadi seperti berikut ini :

<div ref={mapContainer} className="map-container" style={{ height: "350px" }} />

<div className="d-grid gap-2">

    <Link to={`/places/${place.slug}/direction?longitude=${place.longitude}&latitude=${place.latitude}`} className="float-end btn btn-success btn-block btn-md mt-3">
    <i className="fa fa-location-arrow"></i> OPEN DIRECTION
    </Link>
    
</div>

Dari penambahan kode di atas, kita membuat sebuah button baru dengan isi URL /places/:slug/direction?longitude=""&latitude="".

Dimana kita akan mengirimkan 2 parameter tersebut ke dalam halaman map direction sehingga di dalam halaman nanti kita akan tangkap kedua parameter tersebut untuk ditampilkan data-nya di dalam maps.

Sekarang, jika halaman detail place kita reload / refresh, maka akan muncul button baru yang bernama OPEN DIRECTION.

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

Di atas bisa kita lihat pada bagian URL, kita telah mengirimkan 2 parameter, yaitu longitude dan latitude yang di dapatkan dari halaman detail place.

Langkah 2 - Menampilkan Map Direction

Sekarang kita akan belajar manmpilkan map direction dari lokasi kita saat ini menuju lokasi tujuan (place). Silahkan buka file src/pages/web/places/Direction.jsx, kemudian ubah semua kode-nya menjadi seperti berikut ini :

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

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

//import layout web
import LayoutWeb from "../../../layouts/Web";

//import mapbox gl
import mapboxgl from 'mapbox-gl'; // eslint-disable-line import/no-webpack-loader-syntax

//import mapbox gl direction
import Directions from '@mapbox/mapbox-gl-directions/dist/mapbox-gl-directions';

//api key mapbox
mapboxgl.accessToken = import.meta.env.VITE_APP_MAPBOX;

function WebPlaceDirection() {

    //title page
    document.title = "Map Direction - TRAVEL GIS - Website Wisata Berbasis GIS (Geographic Information System)";

    //map container
    const mapContainer = useRef(null);

    //state for user location coordinate
    const [longitude, setLongitude] = useState(110.7241664);
    const [latitude, setLatitude] = useState(-6.9515962);

    //slug params
    const { slug } = useParams();

    //get query params
    const query = new URLSearchParams(useLocation().search);

    //hook
    useEffect(() => {

        //init Map
        const map = new mapboxgl.Map({
            container: mapContainer.current,
            style: 'mapbox://styles/mapbox/streets-v12',
            center: [query.get("longitude"), query.get("latitude")],
            zoom: 15
        });

        //init geolocate
        const geolocate = new mapboxgl.GeolocateControl({
            positionOptions: {
                enableHighAccuracy: true
            },
            // When active the map will receive updates to the device's location as it changes.
            trackUserLocation: true,
            // Draw an arrow next to the location dot to indicate which direction the device is heading.
            showUserHeading: true
        });


        // Add geolocate control to the map.
        map.addControl(geolocate);

        //init directions
        const directions = new Directions({
            accessToken: mapboxgl.accessToken,
            unit: 'metric',
            profile: 'mapbox/driving',
            // UI controls
            controls: {
                inputs: false,
                instructions: true
            },
        });
        
        // Add directions to the map.
        map.on('load', function() {
            geolocate.trigger(); //<- Automatically activates geolocation

            geolocate.on('geolocate', function(position) {
                setLongitude(position.coords.longitude);
                setLatitude(position.coords.latitude);

                //direction
                directions.setOrigin([position.coords.longitude, position.coords.latitude]);
            });
            
            directions.setDestination([query.get("longitude"), query.get("latitude")]);

            map.addControl(directions);
        });

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

    return (
        <React.Fragment>
            <LayoutWeb>
                <div className="container mt-80">
                    <div className="row">
                        <div className="col-md-12 mb-5">
                            <div className="card border-0 rounded shadow-sm">
                              <div className="card-body">
                                <Link to={`/places/${slug}`} className="float-end btn btn-success btn-sm mb-2"><i className="fa fa-long-arrow-alt-left"></i> BACK TO PLACE</Link>
                                <h5><i className="fa fa-location-arrow"></i> DIRECTION FROM USER LOCATION</h5>
                                <hr />
                                <div ref={mapContainer} className="map-container" style={{ height: "400px" }} />
                              </div>
                            </div>
                        </div>
                    </div>
                </div>
            </LayoutWeb>
        </React.Fragment>
    )

}

export default WebPlaceDirection;

Dari penambahan kode di atas, pertama kita import 3 hook dari React, yaitu useState, useEffect dan useRef.

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

Setelah itu, kita import provider Link dan 2 hook dari React Router DOM, yaitu useParams dan useLocation.

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

Dan kita import layout web, karena kita akan gunakan layout ini sebagai induk dari halaman direction yang sedang kita buat.

//import layout web
import LayoutWeb from "../../../layouts/Web";

Kemudian kita import package MapboxGL dan plugin Directions dari Mapbox beserta konfigurasi access token yang diambil dari file .env.

//import mapbox gl
import mapboxgl from 'mapbox-gl'; // eslint-disable-line import/no-webpack-loader-syntax

//import mapbox gl direction
import Directions from '@mapbox/mapbox-gl-directions/dist/mapbox-gl-directions';

//api key mapbox
mapboxgl.accessToken = import.meta.env.VITE_APP_MAPBOX;

Di dalam function component WebPlaceDirection, pertama kita atur title halaman-nya.

//title page
document.title = "Map Direction - TRAVEL GIS - Website Wisata Berbasis GIS (Geographic Information System)";

Kemudian kita inisialisasi sebuah variable baru yang bernama mapContainer dengan isi hook dari useRef. Ini nantinya akan kita gunakan untuk menampilkan maps di dalam element HTML.

//map container
const mapContainer = useRef(null);

Setelah itu, kita buat 2 state baru dengan nama longitude dan latitude dengan kita berikan sebuah default value, karena jika default value-nya kosong maka akan menampilkan sebuah error.

Untuk default value-nya nanti juga akan di replace dengan data yang di dapatkan dari parameter web browser.

//state for user location coordinate
const [longitude, setLongitude] = useState(110.7241664);
const [latitude, setLatitude] = useState(-6.9515962);

Kemudian kita buat object slug dari hook useParams, tujuannya agar mempermudah kita dalam memanggil hook tersebut.

//slug params
const { slug } = useParams();

Dan kita buat variable baru dengan nama query yang isinya adalah method URLSearchParams dan di dalamnya berisi parameter hook useLocation.

//get query params
const query = new URLSearchParams(useLocation().search);

Kemudian di dalam hook useEffect kita melakukan inisialisasi sebuah map dari MapboxGL.

//init Map
const map = new mapboxgl.Map({
    container: mapContainer.current,
    style: 'mapbox://styles/mapbox/streets-v12',
    center: [query.get("longitude"), query.get("latitude")],
    zoom: 15
});

Di atas, untuk value center yang berisi nilai longitude dan latitude kita ambilkan dari query parameter.

Setelah berhasil melakukan inisialisasi map, selanjutnya kita melakukan inisialisasi Geolocation. Ini digunakan untuk mendapatkan lokasi kita saat ini melalui web browser.

//init geolocate
const geolocate = new mapboxgl.GeolocateControl({
    positionOptions: {
        enableHighAccuracy: true
    },
    // When active the map will receive updates to the device's location as it changes.
    trackUserLocation: true,
    // Draw an arrow next to the location dot to indicate which direction the device is heading.
    showUserHeading: true
});

Setelah berhasil diinisialisasi, selanjutnya kita masukkan ke dalam map menggunakan method addControl.

// Add geolocate control to the map.
map.addControl(geolocate);

Kemudian kita inisialisasi lagi untuk map directions, yang mana digunakan untuk menampilkan rute dari 2 lokasi yang berbeda. Yaitu lokasi kita saat ini dengan lokasi place.

//init directions
const directions = new Directions({
    accessToken: mapboxgl.accessToken,
    unit: 'metric',
    profile: 'mapbox/driving',
    // UI controls
    controls: {
        inputs: false,
        instructions: true
    },
});

Di atas, perhatikan bagian profile, dimana kita berikan value mapbox/driving. Yang artinya jarak yang ditampilkan dari kedua rute nanti berdasarkan kendaraan mobil. Kita juga bisa menggantinya dengan beberapa pilihan, seperti :

Kemudian saat map melakukan event load, maka di dalamnya kita memanggil beberapa kondisi. Yang pertama mengaktifkan geolocation.

geolocate.trigger(); //<- Automatically activates geolocation

Saat geolocation berhasil dijalankan, maka akan menjalankan event yang bernama geolocate dan di dalamnya kita melakukan assign nilai longitude dan latitude dari lokasi aktif saat ini ke dalam state.

//set value state `longitude`
setLongitude(position.coords.longitude);

//set value state `latitude`
setLatitude(position.coords.latitude);

Selanjutnya, kita tinggal menampilkan direction dari kedua lokasi, yang pertama kita tampilkan posisi kita sekarang atau biasa disebut origin.

//direction
 directions.setOrigin([position.coords.longitude, position.coords.latitude]);

kemudian kita tampilkan lokasi tujuan atau destination, yang mana isi dari longitude dan latitude diambil dari query parameter.

directions.setDestination([query.get("longitude"), query.get("latitude")]);

Terakhir, kita masukkan konfigurasi direction ke dalam map menggunakan method addControl.

//add directions to map
map.addControl(directions);

Dan untuk menampilkan map di dalam HTML, kita cukup seperti berikut ini :

<div ref={mapContainer} className="map-container" style={{ height: "400px" }} />

Langkah 3 - Uji Coba Map Direction

Sekarang silahkan klik button OPEN DIRECTION di dalam halaman detail place, Jika masih pertama kali maka akan menampilkan notifikasi untuk mengizinkan mengakses lokasi kita saat ini di web browser. Silahkan pilih Allow atau Izinkan. Kurang lebih seperti berikut ini :

Jika sudah dan berhasil, maka akan menampilkan rute dari 2 lokasi yang berbeda dengan informasi jarak yang ditempuh beserta navigasi-nya.

Konfigurasi Route untuk Halaman Maps


Pada materi kali ini kita akan belajar membuat route yang nanatinya digunakan untuk menampilkan halaman maps. Di dalam halaman maps kita akan menampilkan semua data place yang ada dalam bentuk visualisasi maps, sehingga kita bisa lebih mudah dalam melihat lokasi-lokasi data place yang ada.

Langkah 1 - Membuat View/Component Maps

Sebelum membuat konfigurasi route-nya, kita akan membuat file view/component untuk halaman maps terlebih dahulu.

Silahkan buat folder baru dengan nama maps di dalam folder src/pages/web dan di dalam folder maps silahkan buat file baru dengan nama Index.jsx, kemudian masukkan kode berikut ini di dalamnya.

//import react
import React from "react";

//import layout web
import LayoutWeb from "../../../layouts/Web";

function WebMapsIndex() {

    return(
        <React.Fragment>
            <LayoutWeb>
                <div className="container mt-80">
                    Halaman Maps Index
                </div>
            </LayoutWeb>
        </React.Fragment>
    )

}

export default WebMapsIndex;

Di atas, kita telah menambahkan sample kode untuk halaman maps index, dimana kita akan ubah lagi kode-nya dimateri-materi selanjutnya.

Langkah 2 - Konfigurasi Route Maps Index

Setelah berhasil membuat file view/component maps index, langkah selanjutnya adalah membuat konfigurasi 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';

//import view admin sliders Index
import SlidersIndex from '../pages/admin/sliders/Index.jsx';

//import view admin slider Create
import SliderCreate from '../pages/admin/sliders/Create.jsx';

//import view admin users Index
import UsersIndex from '../pages/admin/users/Index.jsx';

//import view admin user Create
import UserCreate from '../pages/admin/users/Create.jsx';

//import view admin user Edit
import UserEdit from '../pages/admin/users/Edit.jsx';

//=======================================================================
//WEB
//=======================================================================

//import view web Home
import Home from '../pages/web/home/Index.jsx';

//import view web category Show
import WebCategoryShow from '../pages/web/categories/Show.jsx';

//import view web place Index
import WebPlacesIndex from "../pages/web/places/Index.jsx";

//import view web place Show
import WebPlaceShow from '../pages/web/places/Show.jsx';

//import view web place Direction
import WebPlaceDirection from '../pages/web/places/Direction.jsx';

//import view web maps
import WebMapsIndex from "../pages/web/maps/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>
                }
            />

            {/* 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>
                }
            />

            {/* private route "/admin/sliders" */}
            <Route
                path="/admin/sliders"
                element={
                        <PrivateRoute>
                            <SlidersIndex />
                        </PrivateRoute>
                }
            />

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

            {/* private route "/admin/users" */}
            <Route
                path="/admin/users"
                element={
                        <PrivateRoute>
                            <UsersIndex />
                        </PrivateRoute>
                }
            />

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

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

            {/* route "/" */}
            <Route path="/" element={<Home />} />

            {/* route "/category/:slug" */}
            <Route path="/category/:slug" element={<WebCategoryShow />} />

            {/* route "/places" */}
            <Route path="/places" element={<WebPlacesIndex />} />

            {/* route "/places/:slug" */}
            <Route path="/places/:slug" element={<WebPlaceShow />} />

            {/* route "/places/:slug/direction" */}
            <Route path="/places/:slug/direction" element={<WebPlaceDirection />} />

            {/* route "/maps" */}
            <Route path="/maps" element={<WebMapsIndex />} />
        </Routes>
    )
}

export default RoutesIndex

Dari perubahan kode di atas, pertama kita melakukan import file view/component maps index.

//import view web maps
import WebMapsIndex from '../pages/web/maps/Index.jsx';

Setelah itu, kita melakukan konfigurasi route-nya dengan path /maps, dimana jika URL tersebut diakses, maka akan memanggil view/component yang bernama <WebMapsIndex />.

Sekarang, untuk memastikan route yang telah kita buat berhasil, kita bisa klik menu MAPS yang ada di navbar atau bisa ke URL berikut ini http://localhost:5173/maps. Jika berhasil maka akan menampilkan hasil seperti berikut ini :

Menampilkan Semua Data Places di Maps


Setelah berhasil membuat konfigurasi route untuk menampilkan halaman maps index, selanjutnya kita akan belajar bagaimana cara menampilkan data place dalam bentuk visualisasi map menggunakan Mapbox.

Jadi kita akan menampilkan semua data place di dalam sebuah map, teknik ini biasa disebut dengan multiple map marker, dimana kita bisa memberikan banyak titik lokasi di dalam sebuah maps (tak terbatas).

Langkah 1 - Edit View/Component Maps Index

Sekarang kita akan melakukan perubahan kode di dalam file view/component. Silahkan buka file src/pages/web/maps/Index.jsx, kemudian ubah semua kode-nya menjadi seperti berikut ini :

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

//import layout web
import LayoutWeb from "../../../layouts/Web";

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

//import mapbox gl
import mapboxgl from 'mapbox-gl'; // eslint-disable-line import/no-webpack-loader-syntax

//api key mapbox
mapboxgl.accessToken = import.meta.env.VITE_APP_MAPBOX;

function WebMapsIndex() {

    //title page
    document.title = "Maps - TRAVEL GIS - Website Wisata Berbasis GIS (Geographic Information System)";

    //map container
    const mapContainer = useRef(null);

    //state coordinate
    const [coordinates, setCoordinates] = useState([]);

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

        //fetching Rest API
        await Api.get('/api/web/all_places')
            .then((response) => {

                //set data to state
                setCoordinates(response.data.data)
            })
    }

    //hook
    useEffect(() => {

        //call function "fetchDataPlaces"
        fetchDataPlaces();

    }, []);

    //hook
    useEffect(() => {

        //init Map
        const map = new mapboxgl.Map({
            container: mapContainer.current,
            style: 'mapbox://styles/mapbox/streets-v12',
            center: [116.5519982204172, -2.8989093904502283],
            zoom: 4
        });

        // Create a default Marker and add it to the map.
        coordinates.forEach((location) => {

            // add popup
            const popup = new mapboxgl.Popup()
                .setHTML(`<h6>${location.title}</h6><hr/><p><i class="fa fa-map-marker"></i> <i>${location.address}</i></p><hr/><div class="d-grid gap-2"><a href="/places/${location.slug}" class="btn btn-sm btn-success btn-block text-white">Lihat Selengkapnya</a></div>`)
                .addTo(map);

            // add marker to map
            new mapboxgl.Marker()
                .setLngLat([location.longitude, location.latitude])
                .setPopup(popup)
                .addTo(map);
        });

    })

    return (
        <React.Fragment>
            <LayoutWeb>
                <div className="container mt-80">
                    <div className="row">
                        <div className="col-md-12 mb-5">
                            <div className="card border-0 rounded shadow-sm">
                              <div className="card-body"><h5><i className="fa fa-map-marked-alt"></i> SEMUA DATA VERSI MAPS</h5>
                                <hr />
                                <div ref={mapContainer} className="map-container" style={{ height: "450px" }} />
                              </div>
                            </div>
                        </div>
                    </div>
                </div>
            </LayoutWeb>
        </React.Fragment>
    )

}

export default WebMapsIndex;

Dari perubahan kode di atas, pertama kita import beberapa hook dari React, seperti useState, useEffect dan useRef.

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

Setelah itu, kita import layout web yang nanti kita gunakan sebagai induk template dari halaman ini.

//import layout web
import LayoutWeb from "../../../layouts/Web";

Karena akan melakukan proses HTTP request, maka kita perlu melakukan import konfigurasi API yang sudah pernah kita buat.

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

Kemudian kita import package MapboxGL dan konfigurasi access token dari file .env.

//import mapbox gl
import mapboxgl from 'mapbox-gl'; // eslint-disable-line import/no-webpack-loader-syntax

//api key mapbox
mapboxgl.accessToken = import.meta.env.VITE_APP_MAPBOX;

Di dalam function component WebMapsIndex pertama kita atur untuk konfigurasi title halaman.

//title page
document.title = "Maps - TRAVEL GIS - Website Wisata Berbasis GIS (Geographic Information System)";

Kemudian kita inisialisasi sebuah variable baru yang bernama mapContainer dengan isi hook dari useRef. Ini nantinya akan kita gunakan untuk menampilkan maps di dalam element HTML.

//map container
const mapContainer = useRef(null);

Stelah itu, kita buat state baru dengan nama coordinates dengan jenis array. State ini akan kita gunakan untuk menyimpan data longitude dan latitude dari semua data place yang ada.

//state coordinate
const [coordinates, setCoordinates] = useState([]);

Kemudian kita buat function baru yang bernama fetchDataPlaces. Di dalam function ini kita melakukan HTTP request ke dalam server dengan endpoint /api/web/places/all.

//fetching Rest API
 await Api.get('/api/web/places/all')

Jika proses fetching berhasil dilakukan, maka kita akan assign response data dari Rest API ke dalam state coordinates.

//set data to state
setCoordinates(response.data.data)

Agar function fetchDataPlaces dapat dijalankan saat halaman diakses, maka kita perlu memanggilnya di dalam hook useEffect.

//hook
useEffect(() => {

    //call function "fetchDataPlaces"
    fetchDataPlaces();

}, []);

Kemudian kita buat inisialisasi hook useEffect baru lagi, yang ini akan kita gunakan untuk melakukan inisialisasi maps di dalamnya. Di dalamnya pertama-tama kita melakukan init maps dari Mapbox.

//init Map
const map = new mapboxgl.Map({
    container: mapContainer.current,
    style: 'mapbox://styles/mapbox/streets-v12',
    center: [116.5519982204172, -2.8989093904502283],
    zoom: 4
});

Pada properti center di atas, kita atur untuk menampilkan seluruh wilayah di negara Indonesia. Dan ini bisa kalian sesuaikan sesuai keinginan.

Setelah itu, kita melakukan perulangan state coordinates.

// Create a default Marker and add it to the map.
coordinates.forEach((location) => {

	//...
	
}

Di mana di dalamnya kita melakukan inisialisasi Popup dari Mapbox.

// add popup
const popup = new mapboxgl.Popup()

Kemudian kita berikan isi dari popup tersebut dengan title dan address dari data place.

.setHTML(`<h6>${location.title}</h6><hr/><p><i class="fa fa-map-marker"></i> <i>${location.address}</i></p><hr/><div class="d-grid gap-2"><a href="/places/${location.slug}" class="btn btn-sm btn-success btn-block text-white">Lihat Selengkapnya</a></div>`)

.addTo(map);

Setelah itu, kita inisialisasi Marker dari Mapbox. Dimana dari hasil popup di atas akan kita marker ke dalam maps.

// add marker to map
new mapboxgl.Marker()
    .setLngLat([location.longitude, location.latitude])
    .setPopup(popup)
    .addTo(map);

Dan untuk menampilkan maps di dalam HTML, kita cukup menggunakan kode seperti berikut ini :

<div ref={mapContainer} className="map-container" style={{ height: "450px" }} />

Langkah 2 - Uji Coba Menampilkan Maps

Sekarang silahkan klik menu MAPS atau bisa ke URL berikut ini http://localhost:5173/maps, jika berhasil maka akan menampilkan kurang lebih seperti berikut ini :

Di atas, kita telah berhasil menampilkan 2 data lokasi place di dalam maps, jika data place lebih banyak, maka data yang ditampilkan juga akan semakin banyak.

Konfigurasi Route untuk Halaman Search


Sekarang kita akan belajar bagaiaman membuat konfigurasi route yang digunakan untuk menampilkan halaman search data place. Halaman ini nantinya akan diakses ketika kita melakukan proses pencarian di dalam website dan kita akan menampilkan data place berdasarkan kata kunci pencarian.

Langkah 1 - Membuat View/Component Search

Sekarang silahkan buat folder baru dengan nama search di dalam folder src/pages/web dan di dalam folder search silahkan buat file baru dengan nama Index.jsx, kemudian masukkan kode berikut ini :

//import react
import React from "react";

//import layout web
import LayoutWeb from "../../../layouts/Web";

function WebSearch() {

    return(
        <React.Fragment>
            <LayoutWeb>
                <div className="container mt-80">
                    Halaman Search
                </div>
            </LayoutWeb>
        </React.Fragment>
    )

}

export default WebSearch;

Dari penambahan kode di atas, kita hanya memberikan sample kode di dalam halaman search, karena nanti kita akan ubah lagi sesuai dengan kebutuhan.

Langkah 2 - Konfigurasi Route Search

Setelah berhasil membuat file view/component untuk halaman search, sekarang kita lanjutkan membuat konfigurasi 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';

//import view admin sliders Index
import SlidersIndex from '../pages/admin/sliders/Index.jsx';

//import view admin slider Create
import SliderCreate from '../pages/admin/sliders/Create.jsx';

//import view admin users Index
import UsersIndex from '../pages/admin/users/Index.jsx';

//import view admin user Create
import UserCreate from '../pages/admin/users/Create.jsx';

//import view admin user Edit
import UserEdit from '../pages/admin/users/Edit.jsx';

//=======================================================================
//WEB
//=======================================================================

//import view web Home
import Home from '../pages/web/home/Index.jsx';

//import view web category Show
import WebCategoryShow from '../pages/web/categories/Show.jsx';

//import view web place Index
import WebPlacesIndex from "../pages/web/places/Index.jsx";

//import view web place Show
import WebPlaceShow from '../pages/web/places/Show.jsx';

//import view web place Direction
import WebPlaceDirection from '../pages/web/places/Direction.jsx';

//import view web maps
import WebMapsIndex from "../pages/web/maps/Index.jsx";

//import view web search
import WebSearch from '../pages/web/search/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>
                }
            />

            {/* 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>
                }
            />

            {/* private route "/admin/sliders" */}
            <Route
                path="/admin/sliders"
                element={
                        <PrivateRoute>
                            <SlidersIndex />
                        </PrivateRoute>
                }
            />

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

            {/* private route "/admin/users" */}
            <Route
                path="/admin/users"
                element={
                        <PrivateRoute>
                            <UsersIndex />
                        </PrivateRoute>
                }
            />

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

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

            {/* route "/" */}
            <Route path="/" element={<Home />} />

            {/* route "/category/:slug" */}
            <Route path="/category/:slug" element={<WebCategoryShow />} />

            {/* route "/places" */}
            <Route path="/places" element={<WebPlacesIndex />} />

            {/* route "/places/:slug" */}
            <Route path="/places/:slug" element={<WebPlaceShow />} />

            {/* route "/places/:slug/direction" */}
            <Route path="/places/:slug/direction" element={<WebPlaceDirection />} />

            {/* route "/maps" */}
            <Route path="/maps" element={<WebMapsIndex />} />

            {/* route "/search" */}
            <Route path="/search" element={<WebSearch />} />

        </Routes>
    )
}

export default RoutesIndex

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

//import view web search
import WebSearch from '../pages/web/search/Index.jsx';

Setelah itu, kita buat konfigurasi route-nya dengan path /search. Dimana jika URL tersebut diakses, maka akan memanggil view/component yang bernama <WebSearch />.

Untuk memastikan bahwa route berhasil dibuat, silahkan buka URL ini http://localhost:5173/search di dalam web browser. Dan jika berhasil maka akan menampilkan halaman seperti berikut ini :

SELESAI & LANJUTKAN

Membuat Fitur Search Data Places


Jika kita perhatikan, di dalam website terdapat 2 menu untuk pencarian data, yaitu di halaman homepage dan di navbar menu. Untuk pencarian di navbar menu nantinya kita akan menggunakan sebuah modal, sehingga tampilannya juga terlihat lebih bagus dan elegan.

Konsep pencarian yang akan kita buat adalah mengirimkan sebuah parameter atau query string di dalam URL browser dan kita arahkan menuju halaman search. Dan di dalam halaman search kita ambil value parameternya dan selanjutnya kita gunakan value tersebut sebagai parameter di dalam endpoint Rest API.

Langkah 1 - Membuat Fitur Search di Halaman Homepage

Sekarang kita akan membuat fitur pencarian di dalam halaman homepage terlebih dahulu. Silahkan buka file src/pages/web/home/Index.jsx, kemudian ubah kode-nya menjadi seperti berikut ini :

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

//import layout web
import LayoutWeb from "../../../layouts/Web";

//import Slider component
import Slider from '../../../components/web/Slider';

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

//import cart category component
import CardCategory from "../../../components/utilities/CardCategory";

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

function Home() {

    //title page
    document.title = "TRAVEL GIS - Website Wisata Berbasis GIS (Geographic Information System)";
    
    //navigate
    const navigate = useNavigate();

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

    //state keyword
    const [keyword, setKeyword] = useState("");

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

        //fetching Rest API
        await Api.get('/api/web/categories')
            .then((response) => {

                //set data to state
                setCategories(response.data.data)
            })
    }

    //hook
    useEffect(() => {

        //call function "fetchDataCategories"
        fetchDataCategories();

    }, []);

    //function "searchHandler"
    const searchHandler = () => {

      //redirect with params "keyword"
      navigate(`/search?q=${keyword}`);
    }

    return (
        <React.Fragment>
          <LayoutWeb>
      
            <Slider />

            <div className="container mb-5">
              <div className="row mt-minus-87">
                <div className="col-md-12">
                  <div className="card border-0 rounded shadow-sm">
                    <div className="card-body">
                      <h5>
                        <i className="fa fa-search"></i> FIND YOUR FAVORITE PLACE
                      </h5>
                      <p>
                        Find your favorite place to vacation with your family!
                      </p>
                      <hr />
                      <input type="text" className="form-control" value={keyword} onChange={(e) => setKeyword(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && searchHandler()} placeholder="find your destination here..." />
                    </div>
                  </div>
                </div>
              </div>
              <div className="row justify-content-center mt-4">
                {
                    categories.map((category) => (
                        <CardCategory 
                          key={category.id}
                          id={category.id} 
                          name={category.name} 
                          slug={category.slug} 
                          image={category.image} 
                        />
                    ))
                }
              </div>
            </div>

          </LayoutWeb>
        </React.Fragment>
    );
}

export default Home;

Dari perubahan kode di atas, pertama kita import hook useNavigate dari React Router DOM.

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

Dan kita buat variable navigate yang berisi hook useNavigate, tujuannya agar mempermudah kita dalam menggunakan hook tersebut.

//navigate
const navigate = useNavigate();

kemudian kita membuat state baru yang bernama keyword, state ini akan digunakan untuk menyimpan kata kunci yang diinputkan di dalam form.

//state keyword
const [keyword, setKeyword] = useState("");

Setelah itu, kita tambahkan event onChange dan onKeyPress.

<input type="text" className="form-control" value={keyword} onChange={(e) => setKeyword(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && searchHandler()} placeholder="find your destination here..." />

Dimana kedua event tersebut akan memanggil function yang bernama searchHandler.

//function "searchHandler"
const searchHandler = () => {

    //redirect with params "keyword"
    navigate(`/search?q=${keyword}`);
}

Dan didalam function searchHandler kita arahkan ke dalam URL /search dengan menambahkan parameter ?q=keyword. Jadi kita arahkan menuju halaman search dengan membawa data kata kunci melalui URL browser.

Sekarang silahkan coba memasukkan kata kunci di halaman home dan tekan enter, maka kita akan diarahkan ke halaman search dengan membawa parameter berupa kata kunci pencarian.

Langkah 2 - Membuat Fitur Search di Navbar

Sebelum kita tampilkan hasil pencarian di dalam halaman search, maka kita akan buat fitur input pencarian lagi di dalam navbar menu. Disini kita akan memanfaatkan modal untuk membuat input pencariannya.

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

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

//import component react bootstrap
import {
    Navbar,
    Container,
    Nav,
    NavDropdown,
    Modal
} from 'react-bootstrap';

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

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

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

function WebHeader() {

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

    //state user logged in
    const [user, setUser] = useState({});

    //modal search
    const [modal, setModal] = useState(false);

    //state keyword
    const [keyword, setKeyword] = useState("");

    //navigate
    const navigate = useNavigate();

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

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

        //fetching Rest API "categories"
        await Api.get('/api/web/categories')
        .then((response) => {

            //set data to state
            setCategories(response.data.data);
        });
    }

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

        //fetching Rest API "user"
        await Api.get('/api/admin/user', {
            headers: {
                //header Bearer + Token
                Authorization: `Bearer ${token}`,
            }
        })
        .then((response) => {

            //set data to state
            setUser(response.data);
        });
    }

    //hook
    useEffect(() => {

        //call function "fetchDataCategories"
        fetchDataCategories();

        //if token already exists
        if(token) {

            //call function "fetchDataUser"
            fetchDataUser();
        }

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

    //function "searchHandler"
    const searchHandler = () => {

        //redirect with params "keyword"
        navigate(`/search?q=${keyword}`);

        //set state modal
        setModal(false);
    }

    return (
        <React.Fragment>
            <Navbar collapseOnSelect expand="lg" className="navbar-custom shadow-sm" fixed="top">
                <Container>
                    <Navbar.Brand as={Link} to="/" className="fw-bold text-white"><i className="fa fa-map-marked-alt"></i> TRAVEL GIS</Navbar.Brand>
                    <Navbar.Toggle aria-controls="responsive-navbar-nav" />
                    <Navbar.Collapse id="responsive-navbar-nav">
                    <Nav className="me-auto">
                        <NavDropdown title={<span><i className="fa fa-list-ul"></i> CATEGORIES</span> } id="collasible-nav-dropdown" className="fw-bold text-white">
                        {
                            categories.map((category) => (
                                <NavDropdown.Item as={Link} to={`/category/${category.slug}`} key={category.id}><img src={category.image} style={{ width: "35px" }} alt=""/> {category.name.toUpperCase()}</NavDropdown.Item>
                            ))
                        }
                        <NavDropdown.Divider />
                        <NavDropdown.Item as={Link} to="/posts/direction">LIHAT LAINNYA <i className="fa fa-long-arrow-alt-right"></i></NavDropdown.Item>
                        </NavDropdown>
                        <Nav.Link as={Link} to="/places" className="fw-bold text-white"><i className="fa fa-globe-asia"></i> PLACES</Nav.Link>
                        <Nav.Link as={Link} to="/maps" className="fw-bold text-white"><i className="fa fa-map"></i> MAPS</Nav.Link>
                    </Nav>
                    <Nav>
                    <Nav.Link onClick={() => setModal(true)} className="fw-bold text-white me-4"><i className="fa fa-search"></i> SEARCH</Nav.Link>
                        {token 
                            ? <Link to="/admin/dashboard" className="btn btn-md btn-light text-uppercase"><i className="fa fa-user-circle"></i> {user.name}</Link>
                            : <Link to="/admin/login" className="btn btn-md btn-light"><i className="fa fa-lock"></i> LOGIN</Link>
                        }
                    </Nav>
                    </Navbar.Collapse>
                </Container>
            </Navbar>
            <Modal
                size="lg"
                show={modal}
                onHide={() => setModal(false)}
                aria-labelledby="example-modal-sizes-title-lg"
            >
                <Modal.Header closeButton>
                <Modal.Title id="example-modal-sizes-title-lg">
                    <i className="fa fa-search"></i> SEARCH
                </Modal.Title>
                </Modal.Header>
                <Modal.Body>
                    <div className="input-group mb-3">
                        <input type="text" className="form-control" value={keyword} onChange={(e) => setKeyword(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && searchHandler()} placeholder="find your destination here..." />
                        <button onClick={searchHandler} type="submit" className="btn btn-md btn-success"><i className="fa fa-search"></i> SEARCH</button>
                    </div>
                </Modal.Body>
            </Modal>
        </React.Fragment>
    );
}

export default WebHeader;

Dari perubahan kode di atas, pertama kita import component Modal dari React Bootstrap.

//import component react bootstrap
import {
    Navbar,
    Container,
    Nav,
    NavDropdown,
    Modal	//<-- component "Modal"
} from 'react-bootstrap';

Setelah itu, kita juga import hook useNavigate dari React Router DOM, ini kita gunakan untuk melakukan navigasi-navigasi ke halaman lain.

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

Kemudian di dalam function component WebHeader, kita menambahkan 2 state baru, yaitu modal dan keyword.

//modal search
const [modal, setModal] = useState(false);

//state keyword
const [keyword, setKeyword] = useState("");

Dan buat variable baru dengan nama navigate, yang berisi hook useNavigate. Agar mempermudah kita dalam menggunakan hook tersebut.

//history
const navigate = useNavigate();

kemudian di dalam icon pencarian kita berikan sebuah event yang bernama onClick, yang mana di dalamnya akan melakukan set state modal menjadi true. Yang artinya modal akan ditampilkan.

<Nav.Link onClick={() => setModal(true)} className="fw-bold text-white me-4"><i className="fa fa-search"></i> SEARCH</Nav.Link>

Dan di dalam modal kita membuat semuah input yang nanti kita gunakan untuk menuliskan kata kunci pencarian.

<input type="text" className="form-control" value={keyword} onChange={(e) => setKeyword(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && searchHandler()} placeholder="find your destination here..." />

<button onClick={searchHandler} type="submit" className="btn btn-md btn-success"><i className="fa fa-search"></i> SEARCH</button>

Jika input di enter atau button di klik, maka akan menjalankan event onChnage dan onClick yang mengarah ke dalam sebuah function yang bernama searchHandler.

Di dalam function searchHandler, kita akan mengarahkan ke halaman search dengan membawa parameter berupa data pencarian yang di dapatkan dari state keyword dan kita set state modal menjadi false atau menutupnya.

//function "searchHandler"
const searchHandler = () => {

    //redirect with params "keyword"
    navigate(`/search?q=${keyword}`);

    //set state modal
    setModal(false);
}

Sekarang, jika kita klik icon pencarian di dalam navbar, maka akan menampilkan sebuah modal yang kurang lebih seperti berikut ini :

Langkah 3 - Menampilkan Data Berdasarkan Pencarian

Sekarang kita lanjutkan untuk menampilkan data place berdasarrkan kata kunci pencarian. Silahkan buka file src/pages/web/search/Index.jsx, kemudian ubah semua kode-nya menjadi seperti berikut ini :

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

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

//import layout web
import LayoutWeb from "../../../layouts/Web";

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

//import card place component
import CardPlace from "../../../components/utilities/CardPlace";

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

function WebSearch() {

    //title page
    document.title = "Search - TRAVEL GIS - Website Wisata Berbasis GIS (Geographic Information System)";

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

    //query params
    const query = new URLSearchParams(useLocation().search);

    //function "fetchDataPlace"
    const fetchDataPlace = async (pageNumber) => {

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

        //fetching Rest API
        await Api.get(`/api/web/places?q=${query.get("q")}&page=${page}`)
        .then((response) => {

            //set data 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 "fetchDataPlace"
        fetchDataPlace();

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [query.get("q")]);

    return (
        <React.Fragment>
            <LayoutWeb>
                <div className="container mt-80">
                    <div className="row">
                        {
                            places.length > 0
                                ? places.map((place) => (
                                    <CardPlace
                                    	key={place.id}
                                        id={place.id}
                                        slug={place.slug}
                                        title={place.title}
                                        images={place.images}
                                        address={place.address}
                                    />
                                ))
                                : <div className="alert alert-danger border-0 rounded shadow-sm" role="alert">
                                    <strong>Opps...!</strong> Data Belum Tersedia!.
                                  </div>
                        }
                    </div>
                    <PaginationComponent 
                        currentPage={currentPage} 
                        perPage={perPage} 
                        total={total} 
                        onChange={(pageNumber) => fetchDataPlace(pageNumber)}
                        position="center"
                    />
                </div>
            </LayoutWeb>
        </React.Fragment>
    )

}

export default WebSearch;

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

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

kemudian kita juga import hook useLocation dari React Router DOM.

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

Dan kita import layout web, karena akan kita gunakan sebagai induk template dari halaman ini.

//import layout web
import LayoutWeb from "../../../layouts/Web";

Karena akan melakukan HTTP request ke dalam server, maka kita butuh import konfigurasi API yang sudah pernah kita buat sebelumnya.

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

Setelah itu, kita import 2 component, yaitu CardPlace dan PaginationComponent.

//import card place component
import CardPlace from "../../../components/utilities/CardPlace";

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

Di dalam function component WebSearch, pertama kita konfigurasi title untuk halaman ini.

//title page
document.title = "Search - TRAVEL GIS - Website Wisata Berbasis GIS (Geographic Information System)";

Kemudian kita buat 4 state baru yang nantinya kita gunakan untuk menyimpan data yang di dapatkan dari 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);

Dan untuk mengambil nilai di parameter URL, maka kita akan buat variable baru dengan nama query yang berisi URLSearchParams dengan parameter hook useLocation.

//query params
const query = new URLSearchParams(useLocation().search);

Kemudian kita buat function baru dengan nama fetchDataPlace dengan parameter pageNumber. Dimana parameter tersebut nanti akan berisi nomor halaman untuk proses pagination.

//function "fetchDataPlace"
const fetchDataPlace = async (pageNumber) => {

	//...
	
}

Di dalam function tersebut pertama kita buat variable baru dengan nama page yangg berisi kondisi menggunakan ternary operator. Jika pageNumber memiliki value, maka itu yang digunakan. Tapi jika kosong, maka akan ambil default dari state yang bernama currentPage.

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

Kemudian kita melakukan HTTP request ke dalam server dengan menambahkan parameter berupa query dari URL browser dan variable page.

//fetching Rest API
await Api.get(`/api/web/places?q=${query.get("q")}&page=${page}`)

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

//set data 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 fetchDataPlace dapat dijalankan saat halaman diakses, maka kita perlu memanggilnya di dalam hook useEffect.

//hook
useEffect(() => {

    //call function "fetchDataPlace"
    fetchDataPlace();

    // eslint-disable-next-line react-hooks/exhaustive-deps
}, [query.get("q")]);

Di atas, kita juga tambahkan [query.get("q")], dimana jika ada perubahan query di URL, maka akan melakukan fetching ulang.

Setelah data berhasil disimpan di dalam state, langkah selanjutnya adalah menampilkan di dalam JSX menggunakan perulangan map. Sebelum itu, kita akan cek terlebih dahulu apakah datanya ada atau tidak di dalam state.

places.length > 0
	? //data tersedia, tampilkan dengan perulangan map
	
	: //data belum tersedia

Di dalam perulangan, kita panggil component CardPlace dengan mengirimkan beberapa data sebagai props, seperti key, id, slug, title, image dan address.

<CardPlace
    key={place.id}
    id={place.id}
    slug={place.slug}
    title={place.title}
    images={place.images}
    address={place.address}
/>

Dan untuk menampilkan navigasi pagination, kita juga memanggil component Pagination dengan mengirimkan beberapa data sebagai props.

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

Langkah 4 - Uji Coba Fitur Pencarian

Sekarang silahkan masukkan kata kunci di dalam input pencarian dan tekan enter, jika berhasil maka akan menampilkan data berdasarkan kata kunci yaang kita masukkan.

Beranda Mundur Maju