Pada tutorial kali ini kita akan menambahkan fitur CRUD lainnya yaitu: Create, Update & Delete. Tidak hanya sekedar menambahkan, kia juga belajar bagaimana caranya menambahkan interaksi drag marker ketika proses edit berlangsung. 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
Part 2 - Tutorial GIS Interaktif Menggunakan Laravel Inertia & React
Step 1: Tambahkan Operasi Create
Sebelum mulai, pastikan data pada table locations sudah dihapus semua agar bisa load data yang sudah diupload nanti-nya.
Tambahkan route create pada web.php:
...
Route::post('/location-create', [LocationController::class, 'create'])->name('location.create');
...
Route::post('/location-create', [LocationController::class, 'create'])->name('location.create');
Navigasi ke LocationController lalu tambahkan method create yang akan menghandle proses create location:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Redirect;
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
]);
}
public function create(Request $request): RedirectResponse{
//validate each request
$validateForm = $request->validate([
'lat' => ['required', 'min:4', 'max:100'],
'long' => ['required', 'min:4', 'max:100'],
'name' => ['required', 'min:3', 'max:255'],
'description' => ['max:500'],
'image' => ['required','mimes:jpg,jpeg,png,avif', 'max:4096'],
'rating' => ['required'],
]);
//define formData
$formData = [
'lat' => $request->lat,
'long' => $request->long,
'name' => $request->name,
'description' => $request->description,
'rating' => $request->rating,
'is_active' => 1
];
//upload image to public/images folder
if($request->image){
$originalImage = $request->image;
$filename = uniqid() . '.' . $request->image->getClientOriginalExtension();
$originalImage->move(public_path().'/images/', $filename);
$formData['image'] = $filename;
}
//create location
Location::create($formData);
return Redirect::route('location.index');
}
}
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Redirect;
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
]);
}
public function create(Request $request): RedirectResponse{
//validate each request
$validateForm = $request->validate([
'lat' => ['required', 'min:4', 'max:100'],
'long' => ['required', 'min:4', 'max:100'],
'name' => ['required', 'min:3', 'max:255'],
'description' => ['max:500'],
'image' => ['required','mimes:jpg,jpeg,png,avif', 'max:4096'],
'rating' => ['required'],
]);
//define formData
$formData = [
'lat' => $request->lat,
'long' => $request->long,
'name' => $request->name,
'description' => $request->description,
'rating' => $request->rating,
'is_active' => 1
];
//upload image to public/images folder
if($request->image){
$originalImage = $request->image;
$filename = uniqid() . '.' . $request->image->getClientOriginalExtension();
$originalImage->move(public_path().'/images/', $filename);
$formData['image'] = $filename;
}
//create location
Location::create($formData);
return Redirect::route('location.index');
}
}
Pindah ke file Index.jsx, panggil component LocationForm (nanti akan kita bikin), tambahkan allLocations sebagai temp state agar kedepan proses update bisa berjalan dengan lancar:
/* ... previous import */
import LocationForm from "@/Components/LocationForm";
import PrimaryButton from "@/Components/PrimaryButton";
const Location = ({ auth }) => {
const { locations } = usePage().props;
const [allLocations, setAllLocations] = useState([]);
const [isCreateMode, setIsCreateMode] = useState(false);
//make sure to update state when locations update from server after create new location
useEffect(() => {
setAllLocations(locations);
}, [locations]);
/* ... previous code */
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="w-3/4">
<Map
reuseMaps
ref={mapRef}
mapboxAccessToken="pk.eyJ1IjoiY3J1c2hlcmJsYWNrIiwiYSI6ImNsOXk3cTZzajAyazYzbnBkbWs0Y3AyNjcifQ.hXpOiJw9u5SzTTbFi-a_zQ"
initialViewState={{
longitude: 106.8291201,
latitude: -6.1836782,
zoom: 12,
bearing: 0,
pitch: 0,
}}
style={{ width: "100%", height: 600 }}
mapStyle="mapbox://styles/mapbox/streets-v9"
cursor={isCreateMode ? "pointer" : "auto"}
>
<GeolocateControl position="top-left" />
<FullscreenControl position="top-left" />
<NavigationControl position="top-left" />
<ScaleControl />
{pins}
{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>
<p>Rating: {popupInfo.rating}</p>
</div>
<img
width="100%"
src={"/images/" + popupInfo.image}
className="object-cover rounded-sm"
/>
</Popup>
)}
</Map>
<div className="mt-8" />
<ItemSlider
locations={allLocations}
ref={sliderRef}
handleJumpTo={handleJumpTo}
setPopupInfo={setPopupInfo}
/>
</div>
<div className="w-1/4">
{!isCreateMode && (
<PrimaryButton className="mb-4" onClick={() => setIsCreateMode(true)}>
Add New Location
</PrimaryButton>
)}
{isCreateMode && (
<p className="text-green-500 font-semibold text-sm mb-2">
Click On Map To Add New Location
</p>
)}
{isCreateMode && <LocationForm />}
</div>
</AuthenticatedLayout>
);
};
export default Location;
/* ... previous import */
import LocationForm from "@/Components/LocationForm";
import PrimaryButton from "@/Components/PrimaryButton";
const Location = ({ auth }) => {
const { locations } = usePage().props;
const [allLocations, setAllLocations] = useState([]);
const [isCreateMode, setIsCreateMode] = useState(false);
//make sure to update state when locations update from server after create new location
useEffect(() => {
setAllLocations(locations);
}, [locations]);
/* ... previous code */
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="w-3/4">
<Map
reuseMaps
ref={mapRef}
mapboxAccessToken="pk.eyJ1IjoiY3J1c2hlcmJsYWNrIiwiYSI6ImNsOXk3cTZzajAyazYzbnBkbWs0Y3AyNjcifQ.hXpOiJw9u5SzTTbFi-a_zQ"
initialViewState={{
longitude: 106.8291201,
latitude: -6.1836782,
zoom: 12,
bearing: 0,
pitch: 0,
}}
style={{ width: "100%", height: 600 }}
mapStyle="mapbox://styles/mapbox/streets-v9"
cursor={isCreateMode ? "pointer" : "auto"}
>
<GeolocateControl position="top-left" />
<FullscreenControl position="top-left" />
<NavigationControl position="top-left" />
<ScaleControl />
{pins}
{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>
<p>Rating: {popupInfo.rating}</p>
</div>
<img
width="100%"
src={"/images/" + popupInfo.image}
className="object-cover rounded-sm"
/>
</Popup>
)}
</Map>
<div className="mt-8" />
<ItemSlider
locations={allLocations}
ref={sliderRef}
handleJumpTo={handleJumpTo}
setPopupInfo={setPopupInfo}
/>
</div>
<div className="w-1/4">
{!isCreateMode && (
<PrimaryButton className="mb-4" onClick={() => setIsCreateMode(true)}>
Add New Location
</PrimaryButton>
)}
{isCreateMode && (
<p className="text-green-500 font-semibold text-sm mb-2">
Click On Map To Add New Location
</p>
)}
{isCreateMode && <LocationForm />}
</div>
</AuthenticatedLayout>
);
};
export default Location;
Buatlah sebuah component LocationForm:
import React from "react";
import { Transition } from "@headlessui/react";
import PrimaryButton from "./PrimaryButton";
import InputError from "./InputError";
import TextInput from "./TextInput";
import InputLabel from "./InputLabel";
const LocationForm = ({
setData = () => {},
handleSubmit = () => {},
handleResetForm = () => {},
data,
errors,
processing,
recentlySuccessful,
}) => {
return (
<form onSubmit={handleSubmit} className="w-full">
<div>
<InputLabel htmlFor="long" value="Longtitude" />
<TextInput
value={data.long}
onChange={(e) => setData("long", e.target.value)}
type="text"
className="mt-1 block w-full"
/>
<InputError message={errors.long} className="mt-2" />
</div>
<div className="mt-4" />
<div>
<InputLabel htmlFor="password" value="Latitude" />
<TextInput
value={data.lat}
onChange={(e) => setData("lat", e.target.value)}
type="text"
className="mt-1 block w-full"
/>
<InputError message={errors.lat} className="mt-2" />
</div>
<div className="mt-4" />
<div>
<InputLabel htmlFor="name" value="Name" />
<TextInput
value={data.name}
onChange={(e) => setData("name", e.target.value)}
type="text"
className="mt-1 block w-full"
/>
<InputError message={errors.name} className="mt-2" />
</div>
<div className="mt-4" />
<div>
<InputLabel htmlFor="description" value="Description" />
<TextInput
value={data.description}
onChange={(e) => setData("description", e.target.value)}
type="text"
className="mt-1 block w-full"
/>
<InputError message={errors.description} className="mt-2" />
</div>
<div className="mt-4" />
<div>
<InputLabel htmlFor="rating" value="Rating" />
<TextInput
value={data.rating}
onChange={(e) => setData("rating", e.target.value)}
type="number"
className="mt-1 block w-full"
/>
<InputError message={errors.rating} className="mt-2" />
</div>
<div className="mt-4" />
<div>
<InputLabel htmlFor="image" value="Image" />
<TextInput
onChange={(e) => setData("image", e.target.files[0])}
type="file"
className="mt-1 block w-full"
/>
<InputError message={errors.image} className="mt-2" />
</div>
<div className="mt-8" />
<div className="flex items-center gap-4">
<PrimaryButton
type="button"
disabled={processing}
onClick={handleResetForm}
>
Cancel
</PrimaryButton>
<PrimaryButton disabled={processing}>Save Location</PrimaryButton>
<Transition
show={recentlySuccessful}
enter="transition ease-in-out"
enterFrom="opacity-0"
leave="transition ease-in-out"
leaveTo="opacity-0"
>
<p className="text-sm text-gray-600 dark:text-gray-400">
New Location Added
</p>
</Transition>
</div>
</form>
);
};
export default LocationForm;
import React from "react";
import { Transition } from "@headlessui/react";
import PrimaryButton from "./PrimaryButton";
import InputError from "./InputError";
import TextInput from "./TextInput";
import InputLabel from "./InputLabel";
const LocationForm = ({
setData = () => {},
handleSubmit = () => {},
handleResetForm = () => {},
data,
errors,
processing,
recentlySuccessful,
}) => {
return (
<form onSubmit={handleSubmit} className="w-full">
<div>
<InputLabel htmlFor="long" value="Longtitude" />
<TextInput
value={data.long}
onChange={(e) => setData("long", e.target.value)}
type="text"
className="mt-1 block w-full"
/>
<InputError message={errors.long} className="mt-2" />
</div>
<div className="mt-4" />
<div>
<InputLabel htmlFor="password" value="Latitude" />
<TextInput
value={data.lat}
onChange={(e) => setData("lat", e.target.value)}
type="text"
className="mt-1 block w-full"
/>
<InputError message={errors.lat} className="mt-2" />
</div>
<div className="mt-4" />
<div>
<InputLabel htmlFor="name" value="Name" />
<TextInput
value={data.name}
onChange={(e) => setData("name", e.target.value)}
type="text"
className="mt-1 block w-full"
/>
<InputError message={errors.name} className="mt-2" />
</div>
<div className="mt-4" />
<div>
<InputLabel htmlFor="description" value="Description" />
<TextInput
value={data.description}
onChange={(e) => setData("description", e.target.value)}
type="text"
className="mt-1 block w-full"
/>
<InputError message={errors.description} className="mt-2" />
</div>
<div className="mt-4" />
<div>
<InputLabel htmlFor="rating" value="Rating" />
<TextInput
value={data.rating}
onChange={(e) => setData("rating", e.target.value)}
type="number"
className="mt-1 block w-full"
/>
<InputError message={errors.rating} className="mt-2" />
</div>
<div className="mt-4" />
<div>
<InputLabel htmlFor="image" value="Image" />
<TextInput
onChange={(e) => setData("image", e.target.files[0])}
type="file"
className="mt-1 block w-full"
/>
<InputError message={errors.image} className="mt-2" />
</div>
<div className="mt-8" />
<div className="flex items-center gap-4">
<PrimaryButton
type="button"
disabled={processing}
onClick={handleResetForm}
>
Cancel
</PrimaryButton>
<PrimaryButton disabled={processing}>Save Location</PrimaryButton>
<Transition
show={recentlySuccessful}
enter="transition ease-in-out"
enterFrom="opacity-0"
leave="transition ease-in-out"
leaveTo="opacity-0"
>
<p className="text-sm text-gray-600 dark:text-gray-400">
New Location Added
</p>
</Transition>
</div>
</form>
);
};
export default LocationForm;
Karena kita sudah load gambar dari database maka kita akan modifikasi component ItemSlider agar support:
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: true,
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);
setPopupInfo(location);
}}
>
<img
src={"/images/" + 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;
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: true,
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);
setPopupInfo(location);
}}
>
<img
src={"/images/" + 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;
Berikut tampilan form setelah klik tombol Tambah Location:
![Create Form](/_next/image?url=%2Fstatic%2Fimages%2Ftutorial%2Fpart-3-tutorial-gis-interaktif-menggunakan-laravel-inertia-react%2Fcreate-form.png&w=2048&q=100)
Tambahkan function simpan location dan teruskan pada component LocationForm
/* ... previous import */
import { Head, useForm, usePage } from "@inertiajs/react";
const Location = ({ auth }) => {
/* ... previous code */
//initialize form and setup HTTP Method to handle save/update form
const {
setData,
post: postHTTPMethod,
delete: deleteHTTPMethod,
reset,
data,
errors,
processing,
recentlySuccessful,
} = useForm({
long: "",
lat: "",
name: "",
description: "",
image: "",
});
//handle to get coordinate from map
const handleSetLocation = useCallback(
(e) => {
if (!isCreateMode) return;
setData({
long: e.lngLat.lng,
lat: e.lngLat.lat,
});
},
[isCreateMode]
);
//handle to save location to database
const handleSubmit = useCallback(
(e) => {
e.preventDefault();
if (isCreateMode) {
postHTTPMethod(route("location.create", data), {
onSuccess: () => {
handleResetForm();
},
});
} else {
//next code here
}
},
[isCreateMode, data]
);
const handleResetForm = useCallback(() => {
setPopupInfo(null);
setIsCreateMode(false);
setAllLocations(locations);
reset();
}, [locations]);
/* ... previous code */
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="w-3/4">
<Map
reuseMaps
ref={mapRef}
mapboxAccessToken="pk.eyJ1IjoiY3J1c2hlcmJsYWNrIiwiYSI6ImNsOXk3cTZzajAyazYzbnBkbWs0Y3AyNjcifQ.hXpOiJw9u5SzTTbFi-a_zQ"
initialViewState={{
longitude: 106.8291201,
latitude: -6.1836782,
zoom: 12,
bearing: 0,
pitch: 0,
}}
style={{ width: "100%", height: 600 }}
mapStyle="mapbox://styles/mapbox/streets-v9"
handleSetLocation={handleSetLocation}
cursor={isCreateMode ? "pointer" : "auto"}
>
{/* ... previous code */}
</Map>
</div>
<div className="w-1/4">
{!isCreateMode && (
<PrimaryButton className="mb-4" onClick={() => setIsCreateMode(true)}>
Add New Location
</PrimaryButton>
)}
{isCreateMode && (
<p className="text-green-500 font-semibold text-sm mb-2">
Click On Map To Add New Location
</p>
)}
{isCreateMode && (
<LocationForm
data={data}
setData={setData}
errors={errors}
handleSubmit={handleSubmit}
handleResetForm={handleResetForm}
processing={processing}
recentlySuccessful={recentlySuccessful}
/>
)}
</div>
</AuthenticatedLayout>
);
};
export default Location;
/* ... previous import */
import { Head, useForm, usePage } from "@inertiajs/react";
const Location = ({ auth }) => {
/* ... previous code */
//initialize form and setup HTTP Method to handle save/update form
const {
setData,
post: postHTTPMethod,
delete: deleteHTTPMethod,
reset,
data,
errors,
processing,
recentlySuccessful,
} = useForm({
long: "",
lat: "",
name: "",
description: "",
image: "",
});
//handle to get coordinate from map
const handleSetLocation = useCallback(
(e) => {
if (!isCreateMode) return;
setData({
long: e.lngLat.lng,
lat: e.lngLat.lat,
});
},
[isCreateMode]
);
//handle to save location to database
const handleSubmit = useCallback(
(e) => {
e.preventDefault();
if (isCreateMode) {
postHTTPMethod(route("location.create", data), {
onSuccess: () => {
handleResetForm();
},
});
} else {
//next code here
}
},
[isCreateMode, data]
);
const handleResetForm = useCallback(() => {
setPopupInfo(null);
setIsCreateMode(false);
setAllLocations(locations);
reset();
}, [locations]);
/* ... previous code */
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="w-3/4">
<Map
reuseMaps
ref={mapRef}
mapboxAccessToken="pk.eyJ1IjoiY3J1c2hlcmJsYWNrIiwiYSI6ImNsOXk3cTZzajAyazYzbnBkbWs0Y3AyNjcifQ.hXpOiJw9u5SzTTbFi-a_zQ"
initialViewState={{
longitude: 106.8291201,
latitude: -6.1836782,
zoom: 12,
bearing: 0,
pitch: 0,
}}
style={{ width: "100%", height: 600 }}
mapStyle="mapbox://styles/mapbox/streets-v9"
handleSetLocation={handleSetLocation}
cursor={isCreateMode ? "pointer" : "auto"}
>
{/* ... previous code */}
</Map>
</div>
<div className="w-1/4">
{!isCreateMode && (
<PrimaryButton className="mb-4" onClick={() => setIsCreateMode(true)}>
Add New Location
</PrimaryButton>
)}
{isCreateMode && (
<p className="text-green-500 font-semibold text-sm mb-2">
Click On Map To Add New Location
</p>
)}
{isCreateMode && (
<LocationForm
data={data}
setData={setData}
errors={errors}
handleSubmit={handleSubmit}
handleResetForm={handleResetForm}
processing={processing}
recentlySuccessful={recentlySuccessful}
/>
)}
</div>
</AuthenticatedLayout>
);
};
export default Location;
Agar bisa menambahkan location baru pastikan kamu sudah mengklik tombol tambah location, kemudian klik ke arah map hingga longtitude dan latitude pada map terisi kemudian klik save location.
Step 2: Tambahkan Operasi Update
Tambahkan sebuah route update pada web.php:
...
Route::post('/location-update', [LocationController::class, 'update'])->name('location.update');
...
Route::post('/location-update', [LocationController::class, 'update'])->name('location.update');
Kembali ke LocationController lalu tambahkan method update yang akan menghandle proses update location:
<?php
namespace App\Http\Controllers;
// ... previous code
class LocationController extends Controller
{
// ... previous code
public function update(Request $request): RedirectResponse{
// find location before update
$location = Location::find($request->id);
$validateForm = $request->validate([
'lat' => ['required', 'min:4', 'max:100'],
'long' => ['required', 'min:4', 'max:100'],
'name' => ['required', 'min:3', 'max:255'],
'description' => ['max:500'],
'image' => ['mimes:jpg,jpeg,png,avif', 'max:4096'],
'rating' => ['required'],
]);
$formData = [
'lat' => $request->lat,
'long' => $request->long,
'name' => $request->name,
'description' => $request->description,
'rating' => $request->rating,
'is_active' => 1
];
//if have previous image delete it and replace with new image
if($request->image){
$imagePath = public_path().'/images/' . $location->image;
if (file_exists($imagePath)) {
unlink($imagePath);
}
$originalImage = $request->image;
$filename = uniqid() . '.' . $request->image->getClientOriginalExtension();
$originalImage->move(public_path().'/images/', $filename);
$formData['image'] = $filename;
}
$location->update($formData);
return Redirect::route('location.index');
}
}
<?php
namespace App\Http\Controllers;
// ... previous code
class LocationController extends Controller
{
// ... previous code
public function update(Request $request): RedirectResponse{
// find location before update
$location = Location::find($request->id);
$validateForm = $request->validate([
'lat' => ['required', 'min:4', 'max:100'],
'long' => ['required', 'min:4', 'max:100'],
'name' => ['required', 'min:3', 'max:255'],
'description' => ['max:500'],
'image' => ['mimes:jpg,jpeg,png,avif', 'max:4096'],
'rating' => ['required'],
]);
$formData = [
'lat' => $request->lat,
'long' => $request->long,
'name' => $request->name,
'description' => $request->description,
'rating' => $request->rating,
'is_active' => 1
];
//if have previous image delete it and replace with new image
if($request->image){
$imagePath = public_path().'/images/' . $location->image;
if (file_exists($imagePath)) {
unlink($imagePath);
}
$originalImage = $request->image;
$filename = uniqid() . '.' . $request->image->getClientOriginalExtension();
$originalImage->move(public_path().'/images/', $filename);
$formData['image'] = $filename;
}
$location->update($formData);
return Redirect::route('location.index');
}
}
Pada Index.jsx tambahkan modifikasi pada function handleJumpTo dan marker agar bisa edit location pada form:
/* ... previous import */
const Location = ({ auth }) => {
/* ... previous code */
const [isUpdateMode, setIsUpdateMode] = useState(false);
/* ... previous code */
const handleJumpTo = useCallback(
(data) => {
setIsUpdateMode(true);
setData({
id: data.id,
lat: data.lat,
long: data.long,
name: data.name,
description: data.description,
rating: data.rating,
});
mapRef.current.easeTo(
{
center: [data.long, data.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, isCreateMode]
);
/* ... previous code */
const pins = useMemo(
() =>
allLocations.map((location, index) => (
<Marker
key={`marker-${index}`}
longitude={location.long}
latitude={location.lat}
anchor="bottom"
onClick={(e) => {
e.originalEvent.stopPropagation();
//make sure user can't click marker when in create mode
if (isCreateMode) return;
setPopupInfo(location);
scrollToSlide(index);
handleJumpTo(location);
}}
draggable={location.id === data.id}
onDragStart={() => setPopupInfo(null)}
onDragEnd={(e) => {
const copiedLocations = [...allLocations];
copiedLocations[index] = {
...copiedLocations[index],
lat: e.lngLat.lat,
long: e.lngLat.lng,
};
setAllLocations(copiedLocations);
setData({
...data,
lat: e.lngLat.lat,
long: e.lngLat.lng,
});
}}
>
<img
src="https://cdn.iconscout.com/icon/free/png-256/free-restaurant-1495593-1267764.png?f=webp"
className={`h-8 w-8 ${
isCreateMode || (isUpdateMode && location.id !== data.id)
? "opacity-40"
: "opacity-100"
}`}
/>
</Marker>
)),
[data, isCreateMode, allLocations]
);
/* ... previous code */
const handleResetForm = useCallback(() => {
setPopupInfo(null);
setIsCreateMode(false);
setIsUpdateMode(false);
setAllLocations(locations);
reset();
}, [locations]);
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="h-max w-3/4">{/*Previous Code*/}</div>
<div className="w-1/4">
{!isCreateMode && !isUpdateMode && (
<PrimaryButton className="mb-4" onClick={() => setIsCreateMode(true)}>
Add New Location
</PrimaryButton>
)}
{isCreateMode && (
<p className="text-green-500 font-semibold text-sm mb-2">
Click On Map To Add New Location
</p>
)}
{(isCreateMode || isUpdateMode) && (
<LocationForm
data={data}
setData={setData}
errors={errors}
handleSubmit={handleSubmit}
handleResetForm={handleResetForm}
processing={processing}
recentlySuccessful={recentlySuccessful}
isUpdateMode={isUpdateMode}
/>
)}
</div>
</AuthenticatedLayout>
);
};
export default Location;
/* ... previous import */
const Location = ({ auth }) => {
/* ... previous code */
const [isUpdateMode, setIsUpdateMode] = useState(false);
/* ... previous code */
const handleJumpTo = useCallback(
(data) => {
setIsUpdateMode(true);
setData({
id: data.id,
lat: data.lat,
long: data.long,
name: data.name,
description: data.description,
rating: data.rating,
});
mapRef.current.easeTo(
{
center: [data.long, data.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, isCreateMode]
);
/* ... previous code */
const pins = useMemo(
() =>
allLocations.map((location, index) => (
<Marker
key={`marker-${index}`}
longitude={location.long}
latitude={location.lat}
anchor="bottom"
onClick={(e) => {
e.originalEvent.stopPropagation();
//make sure user can't click marker when in create mode
if (isCreateMode) return;
setPopupInfo(location);
scrollToSlide(index);
handleJumpTo(location);
}}
draggable={location.id === data.id}
onDragStart={() => setPopupInfo(null)}
onDragEnd={(e) => {
const copiedLocations = [...allLocations];
copiedLocations[index] = {
...copiedLocations[index],
lat: e.lngLat.lat,
long: e.lngLat.lng,
};
setAllLocations(copiedLocations);
setData({
...data,
lat: e.lngLat.lat,
long: e.lngLat.lng,
});
}}
>
<img
src="https://cdn.iconscout.com/icon/free/png-256/free-restaurant-1495593-1267764.png?f=webp"
className={`h-8 w-8 ${
isCreateMode || (isUpdateMode && location.id !== data.id)
? "opacity-40"
: "opacity-100"
}`}
/>
</Marker>
)),
[data, isCreateMode, allLocations]
);
/* ... previous code */
const handleResetForm = useCallback(() => {
setPopupInfo(null);
setIsCreateMode(false);
setIsUpdateMode(false);
setAllLocations(locations);
reset();
}, [locations]);
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="h-max w-3/4">{/*Previous Code*/}</div>
<div className="w-1/4">
{!isCreateMode && !isUpdateMode && (
<PrimaryButton className="mb-4" onClick={() => setIsCreateMode(true)}>
Add New Location
</PrimaryButton>
)}
{isCreateMode && (
<p className="text-green-500 font-semibold text-sm mb-2">
Click On Map To Add New Location
</p>
)}
{(isCreateMode || isUpdateMode) && (
<LocationForm
data={data}
setData={setData}
errors={errors}
handleSubmit={handleSubmit}
handleResetForm={handleResetForm}
processing={processing}
recentlySuccessful={recentlySuccessful}
isUpdateMode={isUpdateMode}
/>
)}
</div>
</AuthenticatedLayout>
);
};
export default Location;
Tambahkan conditional agar tulisan pada button berubah sesuai dengan state update atau create:
/*... previous import*/
const LocationForm = ({
setData = () => {},
handleSubmit = () => {},
handleResetForm = () => {},
handleDeleteLocation = () => {},
data,
errors,
processing,
recentlySuccessful,
isUpdateMode,
}) => {
return (
<form onSubmit={handleSubmit} className="w-full">
{/*Previous Code*/}
<div className="flex items-center gap-4">
<PrimaryButton
type="button"
disabled={processing}
onClick={handleResetForm}
>
Cancel
</PrimaryButton>
<PrimaryButton disabled={processing}>
{isUpdateMode ? "Update" : "Save"} Location
</PrimaryButton>
{/*Previous Code*/}
</div>
</form>
);
};
export default LocationForm;
/*... previous import*/
const LocationForm = ({
setData = () => {},
handleSubmit = () => {},
handleResetForm = () => {},
handleDeleteLocation = () => {},
data,
errors,
processing,
recentlySuccessful,
isUpdateMode,
}) => {
return (
<form onSubmit={handleSubmit} className="w-full">
{/*Previous Code*/}
<div className="flex items-center gap-4">
<PrimaryButton
type="button"
disabled={processing}
onClick={handleResetForm}
>
Cancel
</PrimaryButton>
<PrimaryButton disabled={processing}>
{isUpdateMode ? "Update" : "Save"} Location
</PrimaryButton>
{/*Previous Code*/}
</div>
</form>
);
};
export default LocationForm;
Jika sudah maka kamu dapat mengklik salah satu marker location, lalu tampilan aplikasimu akan me-load data dari map:
![Update Form](/_next/image?url=%2Fstatic%2Fimages%2Ftutorial%2Fpart-3-tutorial-gis-interaktif-menggunakan-laravel-inertia-react%2Fupdateform.png&w=2048&q=100)
Modifikasi function handleSubmit agar bisa update ke database:
/* ... previous import */
const Location = ({ auth }) => {
/* ... previous code */
const handleSubmit = useCallback(
(e) => {
e.preventDefault();
if (isCreateMode) {
postHTTPMethod(route("location.create", data), {
onSuccess: () => {
handleResetForm();
},
});
} else {
postHTTPMethod(route("location.update", data), {
onSuccess: () => {
handleResetForm();
},
});
}
},
[isCreateMode, data]
);
/* ... previous code */
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="h-max w-3/4">{/*Previous Code*/}</div>
{/*Rest of Code*/}
</AuthenticatedLayout>
);
};
export default Location;
/* ... previous import */
const Location = ({ auth }) => {
/* ... previous code */
const handleSubmit = useCallback(
(e) => {
e.preventDefault();
if (isCreateMode) {
postHTTPMethod(route("location.create", data), {
onSuccess: () => {
handleResetForm();
},
});
} else {
postHTTPMethod(route("location.update", data), {
onSuccess: () => {
handleResetForm();
},
});
}
},
[isCreateMode, data]
);
/* ... previous code */
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="h-max w-3/4">{/*Previous Code*/}</div>
{/*Rest of Code*/}
</AuthenticatedLayout>
);
};
export default Location;
Silakan lakukan update pada location dengan cara memilih salah satu marker pada map
Step 3: Tambahkan Operasi Delete
Tambahkan route delete pada web.php:
...
Route::delete('/delete/{id}', [LocationController::class, 'delete'])->name('location.delete');
...
Route::delete('/delete/{id}', [LocationController::class, 'delete'])->name('location.delete');
Kembali ke LocationController lalu tambahkan method delete yang akan menghandle proses delete location:
<?php
namespace App\Http\Controllers;
// ... previous code
class LocationController extends Controller
{
// ... previous code
public function delete($id): RedirectResponse{
$location = Location::find($id);
$location->delete();
return Redirect::route('location.index');
}
}
<?php
namespace App\Http\Controllers;
// ... previous code
class LocationController extends Controller
{
// ... previous code
public function delete($id): RedirectResponse{
$location = Location::find($id);
$location->delete();
return Redirect::route('location.index');
}
}
Pada file Index.jsx tambahkan function delete dan turunkan pada component LocationForm:
/* ... previous import */
const Location = ({ auth }) => {
/* ... previous code */
const handleDeleteLocation = useCallback((id) => {
deleteHTTPMethod(
route("location.delete", {
id,
}),
{
onSuccess: () => {
handleResetForm();
},
}
);
}, []);
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="h-max w-3/4">{/*Previous Code*/}</div>
<div className="w-1/4">
{/*Previous Code*/}
{(isCreateMode || isUpdateMode) && (
<LocationForm
data={data}
setData={setData}
errors={errors}
handleSubmit={handleSubmit}
handleResetForm={handleResetForm}
handleDeleteLocation={handleDeleteLocation}
processing={processing}
recentlySuccessful={recentlySuccessful}
isUpdateMode={isUpdateMode}
/>
)}
</div>
</AuthenticatedLayout>
);
};
export default Location;
/* ... previous import */
const Location = ({ auth }) => {
/* ... previous code */
const handleDeleteLocation = useCallback((id) => {
deleteHTTPMethod(
route("location.delete", {
id,
}),
{
onSuccess: () => {
handleResetForm();
},
}
);
}, []);
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="h-max w-3/4">{/*Previous Code*/}</div>
<div className="w-1/4">
{/*Previous Code*/}
{(isCreateMode || isUpdateMode) && (
<LocationForm
data={data}
setData={setData}
errors={errors}
handleSubmit={handleSubmit}
handleResetForm={handleResetForm}
handleDeleteLocation={handleDeleteLocation}
processing={processing}
recentlySuccessful={recentlySuccessful}
isUpdateMode={isUpdateMode}
/>
)}
</div>
</AuthenticatedLayout>
);
};
export default Location;
Tambahkan logic delete pada component LocationForm
import React from "react";
import { Transition } from "@headlessui/react";
import PrimaryButton from "./PrimaryButton";
import InputError from "./InputError";
import TextInput from "./TextInput";
import InputLabel from "./InputLabel";
const LocationForm = ({
setData = () => {},
handleSubmit = () => {},
handleResetForm = () => {},
handleDeleteLocation = () => {},
data,
errors,
processing,
recentlySuccessful,
isUpdateMode,
}) => {
return (
<form onSubmit={handleSubmit} className="w-full">
{/*Previous Code*/}
<div className="flex items-center gap-4">
<PrimaryButton
type="button"
disabled={processing}
onClick={handleResetForm}
>
Cancel
</PrimaryButton>
<PrimaryButton disabled={processing}>
{isUpdateMode ? "Update" : "Save"} Location
</PrimaryButton>
<Transition
show={recentlySuccessful}
enter="transition ease-in-out"
enterFrom="opacity-0"
leave="transition ease-in-out"
leaveTo="opacity-0"
>
<p className="text-sm text-gray-600 dark:text-gray-400">
New Location Added
</p>
</Transition>
</div>
{isUpdateMode && (
<div className="mt-8">
<DangerButton
type="button"
disabled={processing}
onClick={() => handleDeleteLocation(data.id)}
>
Delete Location
</DangerButton>
</div>
)}
</form>
);
};
export default LocationForm;
import React from "react";
import { Transition } from "@headlessui/react";
import PrimaryButton from "./PrimaryButton";
import InputError from "./InputError";
import TextInput from "./TextInput";
import InputLabel from "./InputLabel";
const LocationForm = ({
setData = () => {},
handleSubmit = () => {},
handleResetForm = () => {},
handleDeleteLocation = () => {},
data,
errors,
processing,
recentlySuccessful,
isUpdateMode,
}) => {
return (
<form onSubmit={handleSubmit} className="w-full">
{/*Previous Code*/}
<div className="flex items-center gap-4">
<PrimaryButton
type="button"
disabled={processing}
onClick={handleResetForm}
>
Cancel
</PrimaryButton>
<PrimaryButton disabled={processing}>
{isUpdateMode ? "Update" : "Save"} Location
</PrimaryButton>
<Transition
show={recentlySuccessful}
enter="transition ease-in-out"
enterFrom="opacity-0"
leave="transition ease-in-out"
leaveTo="opacity-0"
>
<p className="text-sm text-gray-600 dark:text-gray-400">
New Location Added
</p>
</Transition>
</div>
{isUpdateMode && (
<div className="mt-8">
<DangerButton
type="button"
disabled={processing}
onClick={() => handleDeleteLocation(data.id)}
>
Delete Location
</DangerButton>
</div>
)}
</form>
);
};
export default LocationForm;
Kamu dapat melakukan delete dengan cara mengklik salah satu marker location dan pada mode edit klik tombol merah yaitu delete location
![Delete Form](/_next/image?url=%2Fstatic%2Fimages%2Ftutorial%2Fpart-3-tutorial-gis-interaktif-menggunakan-laravel-inertia-react%2Fdelete-form.png&w=2048&q=100)
Penutup
Selesai!!! Selamat kamu sudah menyelesaikan tutorial ini, berikut link repo github dari tutorial ini, jangan lupa di share dan kasih star pada github reponya ya biar kami makin semangat memberikan tutorial gratis lainnya!