T</>TahuCoding

Part 2 - Tutorial GIS Interaktif Menggunakan Laravel Inertia & React - CRUD - Menampilkan Marker

Loading...

Setelah berhasil melakukan setup laravel, inertia.js, react.js dan mapbox, pada tutorial kali ini kita akan belajar bagaimana caranya menampilkan marker pada halaman Master Location.

Berikut demo dari project yang akan kita buat:

Bagi kamu yang belum setup project ini silakan cek tutorial sebelumnya:

Part 1 - Tutorial GIS Interaktif Menggunakan Laravel Inertia & React

Step 1: Tambahkan Controller, Model, Migration & View Master Location

Jalankan command berikut untuk membuat controller dan migration based on model Location:

php artisan make:model Location -mc

Jika berhasil maka laravel akan menggenerate file berikut:

app
Http
Controllers
LocationController.php
Models
Location.php
database
migrations.js
2023_08_15_045938_create_locations_table.php

Navigasi ke LocationController lalu tambahkan code berikut agar dapat merender view master location dan get semua data locations dari database menggunakan inertia:

app\Http\Controllers\LocationController.php
<?php
 
namespace App\Http\Controllers;
 
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
use App\Models\Location;
 
class LocationController extends Controller
{
    public function index(): Response{
        $locations = Location::all();
 
        return Inertia::render('Location/Index', [
            'locations' => $locations
        ]);
    }
}

Tambahkan file Index.jsx pada folder pages:

resources\js\Pages\Location\Index.jsx
import React from "react";
import { Head } from "@inertiajs/react";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
 
const Location = ({ auth }) => {
  return (
    <AuthenticatedLayout
      user={auth.user}
      header={
        <h2 className="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
          Master Location
        </h2>
      }
    >
      <Head title="Dashboard GIS" />
      <div className="py-12">
        <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
          <div>This is location page</div>
        </div>
      </div>
    </AuthenticatedLayout>
  );
};
 
export default Location;

Step 2: Setup Route & Menu Master Location

Pada web route laravel tambahkan rute master location:

routes\web.php
<?php
 
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
 
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\LocationController;
 
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
 
Route::get('/', function () {
    return Inertia::render('Welcome', [
        'canLogin' => Route::has('login'),
        'canRegister' => Route::has('register'),
        'laravelVersion' => Application::VERSION,
        'phpVersion' => PHP_VERSION,
    ]);
});
 
Route::get('/dashboard', function () {
    return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
 
Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
 
    Route::get('/location', [LocationController::class, 'index'])->name('location.index');
});
 
require __DIR__.'/auth.php';
 

Buka file AuthenticatedLayout.jsx lalu tambahkan menu master location seperti berikut:

resources\js\Layouts\AuthenticatedLayout.jsx
...
<Dropdown.Content>
  <Dropdown.Link href={route("profile.edit")}>Profile</Dropdown.Link>
  <Dropdown.Link href={route("profile.edit")}>Master Location</Dropdown.Link>
  <Dropdown.Link href={route("location.index")} method="post" as="button">
    Log Out
  </Dropdown.Link>
</Dropdown.Content>
...

Jika sudah selesai maka kamu akan melihat menu berikut

Master Location Menu Image

Step 3: Setup Migration dan Model Master Location

Setelah berhasil menampilkan halaman master location, pada step ini kita akan menambahkan table locations pada database menggunakan migration dan setup field mana saja yang dapat diinput dari sisi model laravel.

Pergi ke file migration location kemudian tambahkan code berikut untuk menambahkan field pada table locations:

database\migrations\2023_08_15_045938_create_locations_table.php
<?php
 
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
 
return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('locations', function (Blueprint $table) {
            $table->id();
            $table->string("name");
            $table->text("description")->nullable();
            $table->string("lat");
            $table->string("long");
            $table->string("image");
            $table->string("rating");
            $table->boolean("is_active");
            $table->timestamps();
        });
    }
 
    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('locations');
    }
};

Jalankan command berikut untuk memulai migrasi:

php artisan migrate

Selanjutnya, setup model Location seperti berikut:

app\Models\Location.php
<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
 
class Location extends Model
{
    use HasFactory;
 
    protected $fillable = ["name", "description", "lat", "long", "image", "rating", "is_active"];
}
 

Step 4: Tambahkan Seeder Master Location

Selanjutkan buatlah seeder menggunakan command berikut:

php artisan make:seeder LocationSeeder

Kemudian tambahkan data dummy berikut pada LocationSeeder:

database\seeders\LocationSeeder.php
<?php
 
namespace Database\Seeders;
 
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Location;
 
class LocationSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        $locations = [
            [
                'name' => 'Delicious Bites',
                'description' => 'A trendy restaurant offering a fusion of Indonesian and Western cuisines.',
                'image' => 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1a/00/bf/95/buffet-spread.jpg?w=500&h=-1&s=1',
                'lat' => -6.2146,
                'long' => 106.8451,
                'rating' => 4,
                'is_active' => 1
            ],
            [
                'name' => 'Spice Garden',
                'description' => 'Experience the flavors of India in the heart of Jakarta. Enjoy authentic Indian dishes in a vibrant setting.',
                'image' => 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/18/f3/b9/8c/babi-hong.jpg?w=600&h=-1&s=1',
                'lat' => -6.2088,
                'long' => 106.8456,
                'rating' => 2,
                'is_active' => 1
            ],
            [
                'name' => 'Oceanic Delights',
                'description' => 'Savor delectable seafood dishes while enjoying a stunning view of the Jakarta Bay.',
                'image' => 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/18/f3/b7/92/kuluyuk-ayam.jpg?w=600&h=400&s=1',
                'lat' => -6.2263,
                'long' => 106.8308,
                'rating' => 3.5,
                'is_active' => 1
            ],
            [
                'name' => 'Sushi Haven',
                'description' => 'A cozy sushi bar offering an extensive menu of fresh sushi and sashimi.',
                'image' => 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/08/e3/6d/6a/sushi-tei-plaza-indonesia.jpg?w=600&h=400&s=1',
                'lat' => -6.2219,
                'long' => 106.8059,
                'rating' => 5,
                'is_active' => 1
            ],
            [
                'name' => 'La Patisserie',
                'description' => 'Indulge in a wide array of delectable pastries, cakes, and desserts at this charming French-inspired patisserie.',
                'image' => 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/0f/72/01/3b/diponegoro-dining-hall.jpg?w=600&h=400&s=1',
                'lat' => -6.1941,
                'long' => 106.8239,
                'rating' => 3,
                'is_active' => 1
            ],
            [
                'name' => 'Rooftop Lounge',
                'description' => 'Enjoy panoramic views of the city skyline while sipping on handcrafted cocktails and sampling delicious bar bites.',
                'image' => 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/16/b3/5c/28/img20190307155916-largejpg.jpg?w=600&h=400&s=1',
                'lat' => -6.1966,
                'long' => 106.8223,
                'rating' => 5,
                'is_active' => 1
            ],
            [
                'name' => 'Mama Mia Pizza',
                'description' => 'A family-friendly pizzeria serving traditional wood-fired pizzas with a variety of mouthwatering toppings.',
                'image' => 'https://menufyproduction.imgix.net/637865014833715521+765921.png?auto=compress,format&h=1080&w=1920&fit=max',
                'lat' => -6.2189,
                'long' => 106.7998,
                'rating' => 4.5,
                'is_active' => 1
            ],
            [
                'name' => 'Noodle House',
                'description' => 'Step into this cozy noodle house and savor a wide selection of Asian noodle dishes, from ramen to stir-fried noodles.',
                'image' => 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/12/50/6e/59/teras-dharmawangsa.jpg?w=600&h=-1&s=1',
                'lat' => -6.2173,
                'long' => 106.7974,
                'rating' => 4,
                'is_active' => 1
            ],
            [
                'name' => 'The Grill House',
                'description' => 'A steakhouse known for its perfectly grilled steaks, accompaniedby flavorful sauces and sides.',
                'image' => 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/16/85/79/0d/la-grillade.jpg?w=600&h=400&s=1',
                'lat' => -6.2081,
                'long' => 106.7972,
                'rating' => 5,
                'is_active' => 1
            ],
            [
                'name' => 'Cafe Mornings',
                'description' => 'Start your day with a delicious breakfast spread and a cup of freshly brewed coffee at this cozy cafe.',
                'image' => 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1b/f3/24/3c/1f-9.jpg?w=600&h=400&s=1',
                'lat' => -6.2150,
                'long' => 106.8169,
                'rating' => 4.5,
                'is_active' => 1
            ],
        ];
 
        foreach ($locations as $location) {
            Location::create($location);
        }
    }
}

Jalankan seeder agar data dummy di insert pada table locations:

php artisan db:seed --class=LocationSeeder

Step 5: Tambahkan Custom Marker dan Interaksi Map pada Halaman Master Location

Selanjutnya tambahkan library berikut agar map kita punya slider/carausel:

npm install react-slick
npm install slick-carousel

Pada file app.jsx import slick carausel css:

resources\js\app.jsx
import "./bootstrap";
import "../css/app.css";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
 
import { createRoot } from "react-dom/client";
import { createInertiaApp } from "@inertiajs/react";
import { resolvePageComponent } from "laravel-vite-plugin/inertia-helpers";
 
const appName = import.meta.env.VITE_APP_NAME || "Laravel";
 
createInertiaApp({
  title: (title) => `${title} - ${appName}`,
  resolve: (name) =>
    resolvePageComponent(
      `./Pages/${name}.jsx`,
      import.meta.glob("./Pages/**/*.jsx")
    ),
  setup({ el, App, props }) {
    const root = createRoot(el);
 
    root.render(<App {...props} />);
  },
  progress: {
    color: "#4B5563",
  },
});

Modifikasi page Location/Index.jsx menjadi seperti berikut:

resources\js\Pages\Location\index.jsx
import React, { useCallback, useMemo, useRef, useState } from "react";
import { Head, usePage } from "@inertiajs/react";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import Map, {
  FullscreenControl,
  GeolocateControl,
  Marker,
  NavigationControl,
  Popup,
  ScaleControl,
} from "react-map-gl";
import ItemSlider from "@/Components/ItemSlider";
 
const Location = ({ auth }) => {
  const { locations } = usePage().props;
 
  const sliderRef = useRef();
  const mapRef = useRef();
 
  const [popupInfo, setPopupInfo] = useState(null);
 
  // handle scroll image based on clicked pin
  const scrollToSlide = useCallback(
    (index) => {
      sliderRef.current.slickGoTo(index);
    },
    [sliderRef]
  );
 
  // handle jum to long,lat when click on the slider
  const handleJumpTo = useCallback(
    (long, lat) => {
      mapRef.current.easeTo(
        {
          center: [long, lat],
          zoom: 13, // Zoom level of the target location
          bearing: 0, // Bearing of the map (optional)
          pitch: 0, // Pitch of the map (optional)
        },
        {
          duration: 2000, // Animation duration in milliseconds
          easing: (t) => t, // Easing function, default is linear
        }
      );
    },
    [mapRef]
  );
 
  // map all locations to pin, dont forget to use useMemo to improve perfomance
  const pins = useMemo(
    () =>
      locations.map((location, index) => (
        <Marker
          key={`marker-${index}`}
          longitude={location.long}
          latitude={location.lat}
          anchor="bottom"
          onClick={(e) => {
            e.originalEvent.stopPropagation();
            setPopupInfo(location);
            scrollToSlide(index);
            handleJumpTo(location.long, location.lat);
          }}
        >
          <img
            src="https://cdn.iconscout.com/icon/free/png-256/free-restaurant-1495593-1267764.png?f=webp"
            className="h-8 w-8"
          />
        </Marker>
      )),
    []
  );
 
  return (
    <AuthenticatedLayout
      user={auth.user}
      header={
        <h2 className="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
          Master Location
        </h2>
      }
    >
      <Head name="Dashboard GIS" />
      <div className="py-12">
        <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
          <div className="h-max w-3/4">
            <Map
              reuseMaps
              ref={mapRef}
              mapboxAccessToken="YOUR MAPBOX TOKEN"
              initialViewState={{
                longitude: 106.8291201, //Central Jakarta Long
                latitude: -6.1836782, //Central Jakarta Lat
                zoom: 12,
                bearing: 0,
                pitch: 0,
              }}
              style={{ width: "100%", height: 600 }}
              mapStyle="mapbox://styles/mapbox/streets-v9"
            >
              <GeolocateControl position="top-left" />
              <FullscreenControl position="top-left" />
              <NavigationControl position="top-left" />
              <ScaleControl />
              {pins}
              {/* Handle Pop Up when Pin Clicked */}
              {popupInfo && (
                <Popup
                  anchor="top"
                  longitude={Number(popupInfo.long)}
                  latitude={Number(popupInfo.lat)}
                  onClose={() => setPopupInfo(null)}
                >
                  <div className="mb-3">
                    <h2 className="font-semibold mb-2 text-lg">
                      {popupInfo.name}
                    </h2>
                    <p>{popupInfo.description}</p>
                  </div>
                  <img
                    width="100%"
                    src={popupInfo.image}
                    className="object-cover rounded-sm"
                  />
                </Popup>
              )}
            </Map>
          </div>
          {/* Show Slider and navigate to pin when clicked */}
          <div className="mt-8 w-3/4">
            <ItemSlider
              locations={locations}
              ref={sliderRef}
              handleJumpTo={handleJumpTo}
              setPopupInfo={setPopupInfo}
            />
          </div>
        </div>
      </div>
    </AuthenticatedLayout>
  );
};
 
export default Location;

Tambahkan ItemSlider pada folder components:

resources\js\Components\ItemSlider.jsx
import React, { forwardRef } from "react";
import Slider from "react-slick";
 
const settings = {
  dots: false,
  infinite: true,
  speed: 500,
  slidesToShow: 1,
  slidesToScroll: 1,
  arrows: false,
  autoplay: false,
  variableWidth: true,
  pauseOnHover: true,
  swipeToSlide: true,
};
 
const ItemSlider = forwardRef(
  (
    { locations = [], handleJumpTo = () => {}, setPopupInfo = () => {} },
    ref
  ) => {
    return (
      <Slider {...settings} ref={ref}>
        {locations.map((location) => (
          <div
            key={location.name}
            className={"pr-4 h-32 w-40 hover:cursor-pointer hover:opacity-80"}
            onClick={() => {
              handleJumpTo(location.long, location.lat);
              setPopupInfo(location);
            }}
          >
            <img
              src={location.image}
              className="object-cover h-full w-full aspect-video rounded-sm"
            />
            <p className="text-white/80 mt-2 w-40 text-lg">{location.name}</p>
            <p className="text-white/80 mt-1 w-40 text-sm">
              {location.description}
            </p>
          </div>
        ))}
      </Slider>
    );
  }
);
 
export default ItemSlider;

Jika sudah selesai lakukan save, maka tampilan projectmu akan menjadi seperti berikut, kamu dapat mengklik semua marker yang ada untuk memunculkan pop up atau mengklik carausel dibawah untuk pergi menuju lokasi yang diinginkan, tentunya semua marker disini telah di load secara dinamis berdasarkan data yang ada pada table locations.

Master Location Image

Menarik bukan?, selanjutkan kita akan menambahkan fitur CRUD selanjutnya yaitu Create, stay tune dan sampai jumpa kembali!