This commit is contained in:
Faradina 2025-07-14 10:24:47 +07:00
parent 42ad713f36
commit 8fb5e4426d
169 changed files with 44037 additions and 2717 deletions

13
.gitattributes vendored
View File

@ -1,2 +1,11 @@
# Auto detect text files and perform LF normalization * text=auto eol=lf
* text=auto
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

View File

@ -1,2 +1,61 @@
# RECAJE <p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Redberry](https://redberry.international/laravel-development/)**
- **[Active Logic](https://activelogic.com)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View File

@ -14,7 +14,13 @@ class Kernel extends HttpKernel
* @var array<int, class-string|string> * @var array<int, class-string|string>
*/ */
protected $middleware = [ protected $middleware = [
// // \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
]; ];
/** /**
@ -24,22 +30,29 @@ class Kernel extends HttpKernel
*/ */
protected $middlewareGroups = [ protected $middlewareGroups = [
'web' => [ 'web' => [
// Web middleware group \App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
], ],
'api' => [ 'api' => [
// API middleware group // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
], ],
]; ];
/** /**
* The application's route middleware aliases. * The application's route middleware.
* *
* Aliases may be used instead of class names to assign middleware to routes and groups. * These middleware may be assigned to groups or used individually.
* *
* @var array<string, class-string|string> * @var array<string, class-string|string>
*/ */
protected $middlewareAliases = [ protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class, 'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
@ -47,21 +60,10 @@ class Kernel extends HttpKernel
'can' => \Illuminate\Auth\Middleware\Authorize::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'admin' => \App\Http\Middleware\AdminMiddleware::class, 'role' => \App\Http\Middleware\CheckRole::class,
]; 'is_admin' => \App\Http\Middleware\AdminMiddleware::class,
/**
* Register the application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array<string, class-string|string>
*/
protected $routeMiddleware = [
//
]; ];
} }

View File

@ -47,9 +47,9 @@
'url' => env('DB_URL'), 'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'), 'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'), 'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'oyiwebid_recaje'), 'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'oyiwebid_faradina'), 'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', 'Faradina14*'), 'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''), 'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'), 'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),

25
public/.htaccess Normal file
View File

@ -0,0 +1,25 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Handle X-XSRF-Token Header
RewriteCond %{HTTP:x-xsrf-token} .
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Send Requests To Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" zoomAndPan="magnify" viewBox="0 0 750 749.999995" height="1000" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><clipPath id="b632305565"><path d="M 104.71875 26.34375 L 645.46875 26.34375 L 645.46875 723.84375 L 104.71875 723.84375 Z M 104.71875 26.34375 " clip-rule="nonzero"/></clipPath></defs><g clip-path="url(#b632305565)"><path fill="#000000" d="M 645.464844 671.160156 C 645.464844 485.035156 524.316406 334.242188 375.09375 334.242188 C 225.867188 334.242188 104.71875 485.035156 104.71875 671.160156 C 104.71875 700.183594 225.867188 723.84375 375.09375 723.84375 C 524.316406 723.84375 645.464844 700.183594 645.464844 671.160156 Z M 375.09375 26.34375 C 456.488281 26.34375 522.425781 92.277344 522.425781 173.667969 C 522.425781 255.058594 456.488281 320.992188 375.09375 320.992188 C 293.695312 320.992188 227.757812 255.058594 227.757812 173.667969 C 227.757812 92.277344 293.695312 26.34375 375.09375 26.34375 Z M 375.09375 26.34375 " fill-opacity="1" fill-rule="evenodd"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,9 @@
/* Ini akan mencegah papan ketikan (keyboard) muncul pada elemen yang bukan input */
body:not(:focus-within) {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

0
public/favicon.ico Normal file
View File

BIN
public/images/layers-2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/images/layers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

20
public/index.php Normal file
View File

@ -0,0 +1,20 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
// Determine if the application is in maintenance mode...
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
}
// Register the Composer autoloader...
require __DIR__.'/../vendor/autoload.php';
// Bootstrap Laravel and handle the request...
/** @var Application $app */
$app = require_once __DIR__.'/../bootstrap/app.php';
$app->handleRequest(Request::capture());

252
public/js/cafe-search.js Normal file
View File

@ -0,0 +1,252 @@
// Inisialisasi variabel global
let hasSearchResults = false;
let currentStep = 1;
// Global data untuk menyimpan nilai penting setiap kriteria
const importanceValues = {};
// Label untuk tingkat kepentingan
const importanceLabels = {
1: "Sangat tidak penting (1)",
2: "Tidak penting (2)",
3: "Kurang penting (3)",
4: "Agak penting (4)",
5: "Sedang (5)",
6: "Cukup penting (6)",
7: "Penting (7)",
8: "Sangat penting (8)",
9: "Sangat penting sekali (9)",
10: "Paling penting (10)"
};
// Fungsi untuk update nilai slider yang dipanggil langsung dari oninput HTML
function updateSliderValue(id, value) {
// Update nilai penting di object global
importanceValues[id] = parseInt(value);
// Update tampilan label tingkat kepentingan
const importanceDisplay = document.getElementById(`importance_value_${id}`);
if (importanceDisplay) {
importanceDisplay.textContent = importanceLabels[value];
}
// Hitung ulang bobot dan perbarui tampilan
calculateAndUpdateWeights();
}
// Fungsi untuk menghitung bobot berdasarkan tingkat kepentingan
function calculateAndUpdateWeights() {
let totalImportance = 0;
Object.values(importanceValues).forEach(value => {
totalImportance += value;
});
// Hitung bobot berdasarkan proporsi kepentingan
const weights = {};
let totalWeight = 0;
if (totalImportance > 0) {
// Hitung berdasarkan proporsi
Object.keys(importanceValues).forEach(id => {
weights[id] = Math.floor((importanceValues[id] / totalImportance) * 100);
totalWeight += weights[id];
});
// Distribusikan sisa persen
if (totalWeight < 100) {
// Urutkan ID berdasarkan tingkat kepentingan (tertinggi dulu)
const sortedIds = Object.keys(importanceValues).sort((a, b) =>
importanceValues[b] - importanceValues[a]
);
// Distribusikan sisa ke nilai tertinggi
let remainder = 100 - totalWeight;
for (let i = 0; i < remainder; i++) {
weights[sortedIds[i % sortedIds.length]]++;
}
}
} else {
// Distribusi merata jika semua nilai penting adalah 0
const equalWeight = Math.floor(100 / Object.keys(importanceValues).length);
let remainder = 100 - (equalWeight * Object.keys(importanceValues).length);
Object.keys(importanceValues).forEach((id, index) => {
weights[id] = equalWeight + (index < remainder ? 1 : 0);
});
}
// Update tampilan bobot dan input tersembunyi
Object.keys(weights).forEach(id => {
const weightDisplay = document.getElementById(`weight_display_${id}`);
const hiddenInput = document.getElementById(`hidden_weight_${id}`);
if (weightDisplay) {
weightDisplay.textContent = `${weights[id]}%`;
// Terapkan kode warna berdasarkan nilai bobot
if (weights[id] >= 30) {
weightDisplay.className = 'text-sm font-medium text-green-600 dark:text-green-400';
} else if (weights[id] >= 15) {
weightDisplay.className = 'text-sm font-medium text-blue-600 dark:text-blue-400';
} else {
weightDisplay.className = 'text-sm font-medium text-gray-700 dark:text-gray-300';
}
}
if (hiddenInput) {
hiddenInput.value = weights[id];
}
});
// Update indikator total bobot
const totalWeightIndicator = document.getElementById('total_weight_indicator');
if (totalWeightIndicator) {
const calculatedTotal = Object.values(weights).reduce((sum, w) => sum + w, 0);
totalWeightIndicator.textContent = `Total: ${calculatedTotal}%`;
if (calculatedTotal === 100) {
totalWeightIndicator.classList.remove('bg-red-100', 'text-red-800', 'dark:bg-red-900', 'dark:text-red-300');
totalWeightIndicator.classList.add('bg-green-100', 'text-green-800', 'dark:bg-green-900', 'dark:text-green-300');
} else {
totalWeightIndicator.classList.remove('bg-green-100', 'text-green-800', 'dark:bg-green-900', 'dark:text-green-300');
totalWeightIndicator.classList.add('bg-red-100', 'text-red-800', 'dark:bg-red-900', 'dark:text-red-300');
}
}
return Object.values(weights).reduce((sum, w) => sum + w, 0) === 100;
}
// Fungsi untuk cek apakah halaman ini hasil dari submit form
function checkIfSearchResults() {
return hasSearchResults;
}
// Update fungsi showSection untuk memanggil updateStepProgress
function showSection1() {
document.getElementById('section1').style.display = 'block';
document.getElementById('section2').style.display = 'none';
document.getElementById('section3').style.display = 'none';
updateStepperVisually(1);
window.scrollTo(0, 0);
}
function showSection2() {
document.getElementById('section1').style.display = 'none';
document.getElementById('section2').style.display = 'block';
document.getElementById('section3').style.display = 'none';
updateStepperVisually(2);
window.scrollTo(0, 0);
}
function showSection3() {
document.getElementById('section1').style.display = 'none';
document.getElementById('section2').style.display = 'none';
document.getElementById('section3').style.display = 'block';
updateStepperVisually(3);
window.scrollTo(0, 0);
}
// Fungsi untuk memperbarui tampilan stepper
function updateStepperVisually(step) {
// Ambil semua step-item dari stepper
const stepItems = document.querySelectorAll('.step-item');
stepItems.forEach((item, index) => {
const stepNum = index + 1;
const circleDiv = item.querySelector('.w-10');
const textSpan = item.querySelector('.text-sm');
if (stepNum <= step) {
// Langkah aktif atau sudah dilalui
circleDiv.classList.remove('bg-gray-200', 'dark:bg-gray-700', 'text-gray-600', 'dark:text-gray-400');
circleDiv.classList.add('bg-blue-600', 'text-white');
textSpan.classList.remove('text-gray-500', 'dark:text-gray-400');
textSpan.classList.add('text-blue-600', 'dark:text-blue-400');
} else {
// Langkah yang belum aktif
circleDiv.classList.remove('bg-blue-600', 'text-white');
circleDiv.classList.add('bg-gray-200', 'dark:bg-gray-700', 'text-gray-600', 'dark:text-gray-400');
textSpan.classList.remove('text-blue-600', 'dark:text-blue-400');
textSpan.classList.add('text-gray-500', 'dark:text-gray-400');
}
// Update status aktif di class
if (stepNum <= step) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
}
// Fungsi inisialisasi yang akan dipanggil ketika DOM selesai dimuat
function initializeApp() {
try {
console.log('DOM Content Loaded.');
// Ambil data dari atribut script
const scriptTag = document.querySelector('script[src*="cafe-search.js"]');
if (scriptTag) {
hasSearchResults = scriptTag.getAttribute('data-has-search-results') === 'true';
currentStep = parseInt(scriptTag.getAttribute('data-current-step') || '1');
}
// Inisialisasi nilai penting dan bobot dari URL jika ada
const sliders = document.querySelectorAll('.importance-slider');
sliders.forEach(slider => {
const id = slider.dataset.id;
const requestWeight = slider.dataset.requestWeight;
// Jika ada bobot dari request
if (requestWeight) {
// Cari nilai importance yang sesuai dengan bobot
for (let i = 1; i <= 10; i++) {
importanceValues[id] = i;
calculateAndUpdateWeights();
const weight = document.getElementById(`hidden_weight_${id}`).value;
if (parseInt(weight) === parseInt(requestWeight)) {
slider.value = i;
updateSliderValue(id, i);
break;
}
}
} else {
// Default nilai awal
const defaultValue = slider.value;
importanceValues[id] = parseInt(defaultValue);
}
});
// Update semua bobot
calculateAndUpdateWeights();
// Tampilkan section sesuai langkah
if (hasSearchResults) {
showSection3();
} else if (currentStep === 2) {
showSection2();
} else {
showSection1();
}
// Form submission validation - ensure weights sum to 100%
const smartForm = document.getElementById('smartForm');
if (smartForm) {
smartForm.addEventListener('submit', function(event) {
if (!calculateAndUpdateWeights()) {
event.preventDefault();
alert('Total bobot harus 100%. Silakan sesuaikan tingkat kepentingan.');
showSection1();
}
});
}
} catch (error) {
console.error('Terjadi error saat inisialisasi halaman:', error);
}
}
// Inisialisasi saat DOM selesai dimuat
document.addEventListener('DOMContentLoaded', initializeApp);

View File

@ -0,0 +1,23 @@
// Mencegah papan ketikan (keyboard) muncul saat pengguna menyentuh area yang bukan input/textarea
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('touchstart', function(e) {
if (e.target.tagName !== 'INPUT' &&
e.target.tagName !== 'TEXTAREA' &&
!e.target.isContentEditable) {
e.preventDefault();
}
}, { passive: false });
// Menambahkan atribut readonly ke elemen date input untuk mencegah keyboard muncul
const dateInputs = document.querySelectorAll('input[type="date"]');
dateInputs.forEach(input => {
input.setAttribute('readonly', 'readonly');
// Tambahkan event listener untuk menghapus readonly saat diklik
input.addEventListener('mousedown', function() {
this.removeAttribute('readonly');
});
input.addEventListener('blur', function() {
this.setAttribute('readonly', 'readonly');
});
});
});

80
public/js/leaflet-init.js Normal file
View File

@ -0,0 +1,80 @@
// Inisialisasi Leaflet Map untuk halaman tambah kafe
document.addEventListener('DOMContentLoaded', function() {
// Pastikan elemen peta ada
const mapElement = document.getElementById('map');
if (!mapElement) {
console.error('Elemen peta tidak ditemukan!');
return;
}
console.log('Menginisialisasi peta Leaflet...');
// Koordinat default (Alun-Alun Jember)
const defaultLat = -8.1722;
const defaultLng = 113.6982;
try {
// Inisialisasi peta
const mymap = L.map('map').setView([defaultLat, defaultLng], 13);
// Tambahkan layer peta
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(mymap);
// Inisialisasi marker
let marker;
// Fungsi untuk menambahkan marker
function addMarker(lat, lng) {
// Hapus marker sebelumnya jika ada
if (marker) {
mymap.removeLayer(marker);
}
// Tambahkan marker baru
marker = L.marker([lat, lng]).addTo(mymap);
// Update formulir
document.getElementById('latitude').value = lat;
document.getElementById('longitude').value = lng;
document.getElementById('koordinat').value = lat + ', ' + lng;
}
// Event listener untuk klik pada peta
mymap.on('click', function(e) {
console.log('Peta diklik pada:', e.latlng);
addMarker(e.latlng.lat, e.latlng.lng);
});
// Event listener untuk input koordinat manual
const koordinatInput = document.getElementById('koordinat');
if (koordinatInput) {
koordinatInput.addEventListener('change', function() {
const koordinat = this.value.split(',');
if (koordinat.length === 2) {
const lat = parseFloat(koordinat[0].trim());
const lng = parseFloat(koordinat[1].trim());
if (!isNaN(lat) && !isNaN(lng)) {
console.log('Mengubah view peta ke koordinat:', lat, lng);
mymap.setView([lat, lng], 13);
addMarker(lat, lng);
}
}
});
}
// Perbaiki ukuran peta
setTimeout(function() {
mymap.invalidateSize();
console.log('Ukuran peta diperbaiki');
}, 100);
console.log('Peta berhasil diinisialisasi');
} catch (error) {
console.error('Terjadi kesalahan saat menginisialisasi peta:', error);
mapElement.innerHTML = '<div style="text-align: center; padding: 20px; color: red;">Terjadi kesalahan saat memuat peta: ' + error.message + '</div>';
}
});

View File

@ -0,0 +1 @@
.leaflet-control-geocoder{border-radius:4px;background:#fff;min-width:26px;min-height:26px}.leaflet-touch .leaflet-control-geocoder{min-width:30px;min-height:30px}.leaflet-control-geocoder a,.leaflet-control-geocoder .leaflet-control-geocoder-icon{border-bottom:none;display:inline-block}.leaflet-control-geocoder .leaflet-control-geocoder-alternatives a{width:inherit;height:inherit;line-height:inherit}.leaflet-control-geocoder a:hover,.leaflet-control-geocoder .leaflet-control-geocoder-icon:hover{border-bottom:none;display:inline-block}.leaflet-control-geocoder-form{display:none;vertical-align:middle}.leaflet-control-geocoder-expanded .leaflet-control-geocoder-form{display:inline-block}.leaflet-control-geocoder-form input{font-size:120%;border:0;background-color:transparent;width:246px}.leaflet-control-geocoder-icon{border-radius:4px;width:26px;height:26px;border:none;background-color:#fff;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12.2 13l3.4 6.6c.6 1.1 2.5-.4 2-1.2l-4-6.2z'/%3E%3Ccircle cx='10.8' cy='8.9' r='3.9' fill='none' stroke='%23000' stroke-width='1.5'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:center;cursor:pointer}.leaflet-touch .leaflet-control-geocoder-icon{width:30px;height:30px}.leaflet-control-geocoder-throbber .leaflet-control-geocoder-icon{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' stroke='%23000' stroke-linecap='round' stroke-width='1.6' viewBox='0 0 24 24'%3E%3Cdefs/%3E%3Cg%3E%3Cpath stroke-opacity='.1' d='M14 8.4l3-5'/%3E%3Cpath stroke-opacity='.2' d='M15.6 10l5-3'/%3E%3Cpath stroke-opacity='.3' d='M16.2 12H22'/%3E%3Cpath stroke-opacity='.4' d='M15.6 14l5 3m-6.5-1.4l2.9 5'/%3E%3Cpath stroke-opacity='.5' d='M12 16.2V22m-2-6.4l-3 5'/%3E%3Cpath stroke-opacity='.6' d='M8.4 14l-5 3'/%3E%3Cpath stroke-opacity='.7' d='M7.8 12H2'/%3E%3Cpath stroke-opacity='.8' d='M8.4 10l-5-3'/%3E%3Cpath stroke-opacity='.9' d='M10 8.4l-3-5'/%3E%3Cpath d='M12 7.8V2'/%3E%3CanimateTransform attributeName='transform' calcMode='discrete' dur='1s' repeatCount='indefinite' type='rotate' values='0 12 12;30 12 12;60 12 12;90 12 12;120 12 12;150 12 12;180 12 12;210 12 12;240 12 12;270 12 12;300 12 12;330 12 12'/%3E%3C/g%3E%3C/svg%3E")}.leaflet-control-geocoder-form-no-error{display:none}.leaflet-control-geocoder-form input:focus{outline:none}.leaflet-control-geocoder-form button{display:none}.leaflet-control-geocoder-error{margin-top:8px;margin-left:8px;display:block;color:#444}.leaflet-control-geocoder-alternatives{display:block;width:272px;list-style:none;padding:0;margin:0}.leaflet-control-geocoder-alternatives-minimized{display:none;height:0}.leaflet-control-geocoder-alternatives li{white-space:nowrap;display:block;overflow:hidden;padding:5px 8px;text-overflow:ellipsis;border-bottom:1px solid #ccc;cursor:pointer}.leaflet-control-geocoder-alternatives li a,.leaflet-control-geocoder-alternatives li a:hover{width:inherit;height:inherit;line-height:inherit;background:inherit;border-radius:inherit;text-align:left}.leaflet-control-geocoder-alternatives li:last-child{border-bottom:none}.leaflet-control-geocoder-alternatives li:hover,.leaflet-control-geocoder-selected{background-color:#f5f5f5}.leaflet-control-geocoder-address-context{color:#666}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,161 @@
import * as L from 'leaflet';
import { IGeocoder, GeocodingResult } from './geocoders/api';
export interface GeocoderControlOptions extends L.ControlOptions {
/**
* Collapse control unless hovered/clicked
*/
collapsed: boolean;
/**
* How to expand a collapsed control: `touch` or `click` or `hover`
*/
expand: 'touch' | 'click' | 'hover';
/**
* Placeholder text for text input
*/
placeholder: string;
/**
* Message when no result found / geocoding error occurs
*/
errorMessage: string;
/**
* Accessibility label for the search icon used by screen readers
*/
iconLabel: string;
/**
* Object to perform the actual geocoding queries
*/
geocoder?: IGeocoder;
/**
* Immediately show the unique result without prompting for alternatives
*/
showUniqueResult: boolean;
/**
* Show icons for geocoding results (if available); supported by Nominatim
*/
showResultIcons: boolean;
/**
* Minimum number characters before suggest functionality is used (if available from geocoder)
*/
suggestMinLength: number;
/**
* Number of milliseconds after typing stopped before suggest functionality is used (if available from geocoder)
*/
suggestTimeout: number;
/**
* Initial query string for text input
*/
query: string;
/**
* Minimum number of characters in search text before performing a query
*/
queryMinLength: number;
/**
* Whether to mark a geocoding result on the map by default
*/
defaultMarkGeocode: boolean;
}
/**
* Event is fired when selecting a geocode result.
* By default, the control will center the map on it and place a marker at its location.
* To remove the control's default handler for marking a result, set {@link GeocoderControlOptions.defaultMarkGeocode} to `false`.
*/
export type MarkGeocodeEvent = {
geocode: GeocodingResult;
};
export type MarkGeocodeEventHandlerFn = (event: MarkGeocodeEvent) => void;
/**
* Event is fired before invoking {@link IGeocoder.geocode} (or {@link IGeocoder.suggest}).
* The event data contains the query string as `input`.
*/
export type StartGeocodeEvent = {
input: string;
};
export type StartGeocodeEventHandlerFn = (event: StartGeocodeEvent) => void;
/**
* Event is fired before after receiving results from {@link IGeocoder.geocode} (or {@link IGeocoder.suggest}).
* The event data contains the query string as `input` and the geocoding `results`.
*/
export type FinishGeocodeEvent = {
input: string;
results: GeocodingResult[];
};
export type FinishGeocodeEventHandlerFn = (event: FinishGeocodeEvent) => void;
declare module 'leaflet' {
interface Evented {
on(type: 'markgeocode', fn: MarkGeocodeEventHandlerFn, context?: any): this;
on(type: 'startgeocode', fn: StartGeocodeEventHandlerFn, context?: any): this;
on(type: 'startsuggest', fn: StartGeocodeEventHandlerFn, context?: any): this;
on(type: 'finishsuggest', fn: FinishGeocodeEventHandlerFn, context?: any): this;
on(type: 'finishgeocode', fn: FinishGeocodeEventHandlerFn, context?: any): this;
}
}
/**
* Leaflet mixins https://leafletjs.com/reference-1.7.1.html#class-includes
* for TypeScript https://www.typescriptlang.org/docs/handbook/mixins.html
* @internal
*/
declare class EventedControl {
constructor(...args: any[]);
}
/**
* @internal
*/
interface EventedControl extends L.Control, L.Evented {
}
/**
* This is the geocoder control. It works like any other [Leaflet control](https://leafletjs.com/reference.html#control), and is added to the map.
*/
export declare class GeocoderControl extends EventedControl {
options: GeocoderControlOptions;
private _alts;
private _container;
private _errorElement;
private _geocodeMarker;
private _input;
private _lastGeocode;
private _map;
private _preventBlurCollapse;
private _requestCount;
private _results;
private _selection;
private _suggestTimeout;
/**
* Instantiates a geocoder control (to be invoked using `new`)
* @param options the options
*/
constructor(options?: Partial<GeocoderControlOptions>);
addThrobberClass(): void;
removeThrobberClass(): void;
/**
* Returns the container DOM element for the control and add listeners on relevant map events.
* @param map the map instance
* @see https://leafletjs.com/reference.html#control-onadd
*/
onAdd(map: L.Map): HTMLDivElement;
/**
* Sets the query string on the text input
* @param string the query string
*/
setQuery(string: string): this;
private _geocodeResult;
/**
* Marks a geocoding result on the map
* @param result the geocoding result
*/
markGeocode(event: MarkGeocodeEvent): this;
private _geocode;
private _geocodeResultSelected;
private _toggle;
private _expand;
private _collapse;
private _clearResults;
private _createAlt;
private _keydown;
private _change;
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link GeocoderControl}
* @param options the options
*/
export declare function geocoder(options?: Partial<GeocoderControlOptions>): GeocoderControl;
export {};

View File

@ -0,0 +1,77 @@
import * as L from 'leaflet';
/**
* An object that represents a result from a geocoding query
*/
export interface GeocodingResult {
/**
* Name of found location
*/
name: string;
/**
* The bounds of the location
*/
bbox: L.LatLngBounds;
/**
* The center coordinate of the location
*/
center: L.LatLng;
/**
* URL for icon representing result; optional
*/
icon?: string;
/**
* HTML formatted representation of the name
*/
html?: string;
/**
* Additional properties returned by the geocoder
*/
properties?: any;
}
/**
* An interface implemented to respond to geocoding queries
*/
export interface IGeocoder {
/**
* Performs a geocoding query and returns the results as promise
* @param query the query
*/
geocode(query: string): Promise<GeocodingResult[]>;
/**
* Performs a geocoding query suggestion (this happens while typing) and returns the results as promise
* @param query the query
*/
suggest?(query: string): Promise<GeocodingResult[]>;
/**
* Performs a reverse geocoding query and returns the results as promise
* @param location the coordinate to reverse geocode
* @param scale the map scale possibly used for reverse geocoding
*/
reverse?(location: L.LatLngLiteral, scale: number): Promise<GeocodingResult[]>;
}
export interface GeocoderOptions {
/**
* URL of the service
*/
serviceUrl: string;
/**
* Additional URL parameters (strings) that will be added to geocoding requests
*/
geocodingQueryParams?: Record<string, unknown>;
/**
* Additional URL parameters (strings) that will be added to reverse geocoding requests
*/
reverseQueryParams?: Record<string, unknown>;
/**
* API key to use this service
*/
apiKey?: string;
}
/**
* @internal
*/
export declare function geocodingParams(options: GeocoderOptions, params: Record<string, unknown>): Record<string, unknown>;
/**
* @internal
*/
export declare function reverseParams(options: GeocoderOptions, params: Record<string, unknown>): Record<string, unknown>;

View File

@ -0,0 +1,47 @@
import * as L from 'leaflet';
import { IGeocoder, GeocoderOptions, GeocodingResult } from './api';
export interface ArcGisOptions extends GeocoderOptions {
}
/**
* Implementation of the [ArcGIS geocoder](https://developers.arcgis.com/features/geocoding/)
*/
export declare class ArcGis implements IGeocoder {
options: ArcGisOptions;
constructor(options?: Partial<ArcGisOptions>);
geocode(query: string): Promise<GeocodingResult[]>;
suggest(query: string): Promise<GeocodingResult[]>;
reverse(location: L.LatLngLiteral, scale: number): Promise<GeocodingResult[]>;
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link ArcGis}
* @param options the options
*/
export declare function arcgis(options?: Partial<ArcGisOptions>): ArcGis;
/**
* @internal
*/
export interface ArcGisResponse {
spatialReference: {
wkid: number;
latestWkid: number;
};
candidates: Candidate[];
}
interface Candidate {
address: string;
location: {
x: number;
y: number;
};
score: number;
attributes: {
Addr_Type: string;
};
extent: {
xmin: number;
ymin: number;
xmax: number;
ymax: number;
};
}
export {};

View File

@ -0,0 +1,84 @@
import * as L from 'leaflet';
import { GeocodingResult, IGeocoder } from './api';
export interface AzureMapsOptions {
apiKey: string;
serviceUrl: string;
}
/**
* Implementation of [Azure Maps Geocoding](https://www.microsoft.com/en-us/maps/azure/location-services/geocoding)
*
* https://learn.microsoft.com/en-us/rest/api/maps/search?view=rest-maps-1.0
*/
export declare class AzureMaps implements IGeocoder {
private options;
constructor(options: Partial<AzureMapsOptions>);
/**
* {@inheritdoc}
* https://learn.microsoft.com/en-us/rest/api/maps/search/get-search-address?view=rest-maps-1.0&tabs=HTTP
*/
geocode(query: string): Promise<GeocodingResult[]>;
/**
* {@inheritdoc}
* https://learn.microsoft.com/en-us/rest/api/maps/search/get-search-address-reverse?view=rest-maps-1.0&tabs=HTTP
*/
reverse(location: L.LatLngLiteral, scale: number): Promise<GeocodingResult[]>;
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link Azure}
* @param options the options
*/
export declare function azure(options: AzureMapsOptions): AzureMaps;
/**
* @internal
*/
export interface AzureMapsResponse {
summary: Summary;
results: Result[];
}
interface Result {
type: string;
id: string;
score: number;
address: Address;
position: Position;
viewport: Viewport;
entryPoints: EntryPoint[];
}
interface Address {
streetNumber: string;
streetName: string;
municipalitySubdivision: string;
municipality: string;
countrySecondarySubdivision: string;
countryTertiarySubdivision: string;
countrySubdivisionCode: string;
postalCode: string;
extendedPostalCode: string;
countryCode: string;
country: string;
countryCodeISO3: string;
freeformAddress: string;
countrySubdivisionName: string;
}
interface EntryPoint {
type: string;
position: Position;
}
interface Position {
lat: number;
lon: number;
}
interface Viewport {
topLeftPoint: Position;
btmRightPoint: Position;
}
interface Summary {
query: string;
queryType: string;
queryTime: number;
numResults: number;
offset: number;
totalResults: number;
fuzzyLevel: number;
}
export {};

View File

@ -0,0 +1,22 @@
import * as L from 'leaflet';
import { IGeocoder, GeocoderOptions, GeocodingResult } from './api';
export interface BingOptions extends GeocoderOptions {
}
/**
* Implementation of the [Bing Locations API](https://docs.microsoft.com/en-us/bingmaps/rest-services/locations/)
*
* Bing Maps for Enterprise is deprecated and will be retired.
* Free (Basic) account customers can continue to use Bing Maps for Enterprise services until June 30th, 2025.
* Enterprise account customers can continue to use Bing Maps for Enterprise services until June 30th, 2028.
*/
export declare class Bing implements IGeocoder {
options: BingOptions;
constructor(options?: Partial<BingOptions>);
geocode(query: string): Promise<GeocodingResult[]>;
reverse(location: L.LatLngLiteral, scale: number): Promise<GeocodingResult[]>;
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link Bing}
* @param options the options
*/
export declare function bing(options?: Partial<BingOptions>): Bing;

View File

@ -0,0 +1,52 @@
import * as L from 'leaflet';
import { IGeocoder, GeocoderOptions, GeocodingResult } from './api';
/**
* Implementation of the [Google Geocoding API](https://developers.google.com/maps/documentation/geocoding/)
*/
export interface GoogleOptions extends GeocoderOptions {
}
export declare class Google implements IGeocoder {
options: GoogleOptions;
constructor(options?: Partial<GoogleOptions>);
geocode(query: string): Promise<GeocodingResult[]>;
reverse(location: L.LatLngLiteral, scale: number): Promise<GeocodingResult[]>;
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link Google}
* @param options the options
*/
export declare function google(options?: Partial<GoogleOptions>): Google;
/**
* @internal
*/
export interface GoogleResponse {
results: Result[];
status: string;
}
interface Result {
address_components: AddressComponent[];
formatted_address: string;
geometry: Geometry;
place_id: string;
types: string[];
}
interface AddressComponent {
long_name: string;
short_name: string;
types: string[];
}
interface Geometry {
bounds: Bounds;
location: Location;
location_type: string;
viewport: Bounds;
}
interface Bounds {
northeast: Location;
southwest: Location;
}
interface Location {
lat: number;
lng: number;
}
export {};

View File

@ -0,0 +1,119 @@
import * as L from 'leaflet';
import { IGeocoder, GeocoderOptions, GeocodingResult } from './api';
export interface HereOptions extends GeocoderOptions {
/**
* Use `apiKey` and the new `HEREv2` geocoder
* @deprecated
*/
app_id: string;
/**
* Use `apiKey` and the new `HEREv2` geocoder
* @deprecated
*/
app_code: string;
reverseGeocodeProxRadius?: any;
apiKey: string;
maxResults: number;
}
/**
* Implementation of the [HERE Geocoder API](https://developer.here.com/documentation/geocoder/topics/introduction.html)
*/
export declare class HERE implements IGeocoder {
options: HereOptions;
constructor(options?: Partial<HereOptions>);
geocode(query: string): Promise<GeocodingResult[]>;
reverse(location: L.LatLngLiteral, scale: number): Promise<GeocodingResult[]>;
getJSON(url: string, params: any): Promise<GeocodingResult[]>;
}
/**
* Implementation of the new [HERE Geocoder API](https://developer.here.com/documentation/geocoding-search-api/api-reference-swagger.html)
*/
export declare class HEREv2 implements IGeocoder {
options: HereOptions;
constructor(options?: Partial<HereOptions>);
geocode(query: string): Promise<GeocodingResult[]>;
reverse(location: L.LatLngLiteral, scale: number): Promise<GeocodingResult[]>;
getJSON(url: string, params: any): Promise<GeocodingResult[]>;
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link HERE}
* @param options the options
*/
export declare function here(options?: Partial<HereOptions>): HERE | HEREv2;
/**
* @internal
*/
export interface HEREv2Response {
items: Item[];
}
interface Item {
title: string;
id: string;
ontologyId: string;
resultType: string;
address: Address;
mapView?: MapView;
position: Position;
access: Position[];
distance: number;
categories: Category[];
references: Reference[];
foodTypes: Category[];
contacts: Contact[];
openingHours: OpeningHour[];
}
interface MapView {
east: number;
north: number;
south: number;
west: number;
}
interface Position {
lat: number;
lng: number;
}
interface Address {
label: string;
countryCode: string;
countryName: string;
stateCode: string;
state: string;
county: string;
city: string;
district: string;
street: string;
postalCode: string;
houseNumber: string;
}
interface Category {
id: string;
name: string;
primary?: boolean;
}
interface Contact {
phone: Email[];
fax: Email[];
www: Email[];
email: Email[];
}
interface Email {
value: string;
}
interface OpeningHour {
text: string[];
isOpen: boolean;
structured: Structured[];
}
interface Structured {
start: string;
duration: string;
recurrence: string;
}
interface Reference {
supplier: Supplier;
id: string;
}
interface Supplier {
id: string;
}
export {};

View File

@ -0,0 +1,16 @@
export * from './api';
export * from './arcgis';
export * from './bing';
export * from './azure';
export * from './google';
export * from './here';
export * from './latlng';
export * from './mapbox';
export * from './mapquest';
export * from './neutrino';
export * from './nominatim';
export * from './open-location-code';
export * from './opencage';
export * from './pelias';
export * from './photon';
export * from './what3words';

View File

@ -0,0 +1,31 @@
import * as L from 'leaflet';
import { IGeocoder, GeocodingResult } from './api';
export interface LatLngOptions {
/**
* The next geocoder to use for non-supported queries
*/
next?: IGeocoder;
/**
* The size in meters used for passing to `LatLng.toBounds`
*/
sizeInMeters: number;
}
/**
* Parses basic latitude/longitude strings such as `'50.06773 14.37742'`, `'N50.06773 W14.37742'`, `'S 50° 04.064 E 014° 22.645'`, or `'S 50° 4 03.828″, W 14° 22 38.712″'`
* @param query the latitude/longitude string to parse
* @returns the parsed latitude/longitude
*/
export declare function parseLatLng(query: string): L.LatLng | undefined;
/**
* Parses basic latitude/longitude strings such as `'50.06773 14.37742'`, `'N50.06773 W14.37742'`, `'S 50° 04.064 E 014° 22.645'`, or `'S 50° 4 03.828″, W 14° 22 38.712″'`
*/
export declare class LatLng implements IGeocoder {
options: LatLngOptions;
constructor(options?: Partial<LatLngOptions>);
geocode(query: string): Promise<GeocodingResult[]>;
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link LatLng}
* @param options the options
*/
export declare function latLng(options?: Partial<LatLngOptions>): LatLng;

View File

@ -0,0 +1,64 @@
import * as L from 'leaflet';
import { IGeocoder, GeocoderOptions, GeocodingResult } from './api';
export interface MapboxOptions extends GeocoderOptions {
}
/**
* Implementation of the [Mapbox Geocoding](https://www.mapbox.com/api-documentation/#geocoding)
*/
export declare class Mapbox implements IGeocoder {
options: MapboxOptions;
constructor(options?: Partial<MapboxOptions>);
_getProperties(loc: any): {
text: any;
address: any;
};
geocode(query: string): Promise<GeocodingResult[]>;
suggest(query: string): Promise<GeocodingResult[]>;
reverse(location: L.LatLngLiteral, scale: number): Promise<GeocodingResult[]>;
private _parseResults;
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link Mapbox}
* @param options the options
*/
export declare function mapbox(options?: Partial<MapboxOptions>): Mapbox;
/**
* @internal
*/
export interface MapboxResponse {
type: string;
query: string[];
features: Feature[];
attribution: string;
}
interface Feature {
id: string;
type: string;
place_type: string[];
relevance: number;
properties: Properties;
text: string;
place_name: string;
matching_text: string;
matching_place_name: string;
center: [number, number];
bbox?: [number, number, number, number];
geometry: Geometry;
address: string;
context: Context[];
}
interface Context {
id: string;
text: string;
wikidata?: string;
short_code?: string;
}
interface Geometry {
type: string;
coordinates: number[];
interpolated: boolean;
omitted: boolean;
}
interface Properties {
}
export {};

View File

@ -0,0 +1,20 @@
import * as L from 'leaflet';
import { IGeocoder, GeocoderOptions, GeocodingResult } from './api';
export interface MapQuestOptions extends GeocoderOptions {
}
/**
* Implementation of the [MapQuest Geocoding API](http://developer.mapquest.com/web/products/dev-services/geocoding-ws)
*/
export declare class MapQuest implements IGeocoder {
options: MapQuestOptions;
constructor(options?: Partial<MapQuestOptions>);
_formatName(...parts: string[]): string;
geocode(query: string): Promise<GeocodingResult[]>;
reverse(location: L.LatLngLiteral, scale: number): Promise<GeocodingResult[]>;
private _parseResults;
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link MapQuest}
* @param options the options
*/
export declare function mapQuest(options?: Partial<MapQuestOptions>): MapQuest;

View File

@ -0,0 +1,20 @@
import * as L from 'leaflet';
import { IGeocoder, GeocoderOptions, GeocodingResult } from './api';
export interface NeutrinoOptions extends GeocoderOptions {
userId: string;
}
/**
* Implementation of the [Neutrino API](https://www.neutrinoapi.com/api/geocode-address/)
*/
export declare class Neutrino implements IGeocoder {
options: NeutrinoOptions;
constructor(options?: Partial<NeutrinoOptions>);
geocode(query: string): Promise<GeocodingResult[]>;
suggest(query: string): Promise<GeocodingResult[]>;
reverse(location: L.LatLngLiteral, scale: number): Promise<GeocodingResult[]>;
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link Neutrino}
* @param options the options
*/
export declare function neutrino(options?: Partial<NeutrinoOptions>): Neutrino;

View File

@ -0,0 +1,63 @@
import * as L from 'leaflet';
import { IGeocoder, GeocoderOptions, GeocodingResult } from './api';
export type NominatimResponse = NominatimResult[];
export interface NominatimResult {
place_id: number;
licence: string;
osm_type: string;
osm_id: number;
boundingbox: string[];
lat: string;
lon: string;
display_name: string;
class?: string;
type?: string;
importance?: number;
icon?: string;
address: NominatimAddress;
}
export interface NominatimAddress {
building?: string;
city_district?: string;
city?: string;
country_code?: string;
country?: string;
county?: string;
hamlet?: string;
house_number?: string;
neighbourhood?: string;
postcode?: string;
road?: string;
state_district?: string;
state?: string;
suburb?: string;
village?: string;
}
export interface NominatimOptions extends GeocoderOptions {
/**
* Additional URL parameters (strings) that will be added to geocoding requests; can be used to restrict results to a specific country for example, by providing the [`countrycodes`](https://wiki.openstreetmap.org/wiki/Nominatim#Parameters) parameter to Nominatim
*/
geocodingQueryParams?: Record<string, unknown>;
/**
* A function that takes an GeocodingResult as argument and returns an HTML formatted string that represents the result. Default function breaks up address in parts from most to least specific, in attempt to increase readability compared to Nominatim's naming
*/
htmlTemplate: (r: NominatimResult) => string;
}
/**
* Implementation of the [Nominatim](https://wiki.openstreetmap.org/wiki/Nominatim) geocoder.
*
* This is the default geocoding service used by the control, unless otherwise specified in the options.
*
* Unless using your own Nominatim installation, please refer to the [Nominatim usage policy](https://operations.osmfoundation.org/policies/nominatim/).
*/
export declare class Nominatim implements IGeocoder {
options: NominatimOptions;
constructor(options?: Partial<NominatimOptions>);
geocode(query: string): Promise<GeocodingResult[]>;
reverse(location: L.LatLngLiteral, scale: number): Promise<GeocodingResult[]>;
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link Nominatim}
* @param options the options
*/
export declare function nominatim(options?: Partial<NominatimOptions>): Nominatim;

View File

@ -0,0 +1,37 @@
import * as L from 'leaflet';
import { IGeocoder, GeocodingResult } from './api';
export interface OpenLocationCodeOptions {
OpenLocationCode: OpenLocationCodeApi;
codeLength?: number;
}
export interface OpenLocationCodeApi {
encode(latitude: number, longitude: number, codeLength?: number): string;
decode(code: string): CodeArea;
}
export interface CodeArea {
latitudeLo: number;
longitudeLo: number;
latitudeHi: number;
longitudeHi: number;
latitudeCenter: number;
longitudeCenter: number;
codeLength: number;
}
/**
* Implementation of the [Plus codes](https://plus.codes/) (formerly OpenLocationCode) (requires [open-location-code](https://www.npmjs.com/package/open-location-code))
*/
export declare class OpenLocationCode implements IGeocoder {
options: OpenLocationCodeOptions;
constructor(options?: Partial<OpenLocationCodeOptions>);
geocode(query: string): Promise<GeocodingResult[]>;
reverse(location: L.LatLngLiteral, scale: number): Promise<{
name: string;
center: L.LatLng;
bbox: L.LatLngBounds;
}[]>;
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link OpenLocationCode}
* @param options the options
*/
export declare function openLocationCode(options?: Partial<OpenLocationCodeOptions>): OpenLocationCode;

View File

@ -0,0 +1,16 @@
import * as L from 'leaflet';
import { IGeocoder, GeocoderOptions, GeocodingResult } from './api';
export interface OpenCageOptions extends GeocoderOptions {
}
/**
* Implementation of the [OpenCage Data API](https://opencagedata.com/)
*/
export declare class OpenCage implements IGeocoder {
options: OpenCageOptions;
constructor(options?: Partial<OpenCageOptions>);
geocode(query: string): Promise<GeocodingResult[]>;
suggest(query: string): Promise<GeocodingResult[]>;
reverse(location: L.LatLngLiteral, scale: number): Promise<GeocodingResult[]>;
private _parseResults;
}
export declare function opencage(options?: Partial<OpenCageOptions>): OpenCage;

View File

@ -0,0 +1,83 @@
import * as L from 'leaflet';
import { IGeocoder, GeocoderOptions, GeocodingResult } from './api';
export interface PeliasOptions extends GeocoderOptions {
}
/**
* Implementation of the [Pelias](https://pelias.io/), [geocode.earth](https://geocode.earth/) geocoder (formerly Mapzen Search)
*/
export declare class Pelias implements IGeocoder {
options: PeliasOptions;
constructor(options?: Partial<PeliasOptions>);
geocode(query: string): Promise<GeocodingResult[]>;
suggest(query: string): Promise<GeocodingResult[]>;
reverse(location: L.LatLngLiteral, scale: number): Promise<GeocodingResult[]>;
_parseResults(data: any, bboxname: any): GeocodingResult[];
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link Pelias}
* @param options the options
*/
export declare function pelias(options?: Partial<PeliasOptions>): Pelias;
export declare const GeocodeEarth: typeof Pelias;
export declare const geocodeEarth: typeof pelias;
/**
* r.i.p.
* @deprecated
*/
export declare const Mapzen: typeof Pelias;
/**
* r.i.p.
* @deprecated
*/
export declare const mapzen: typeof pelias;
/**
* Implementation of the [Openrouteservice](https://openrouteservice.org/dev/#/api-docs/geocode) geocoder
*/
export declare class Openrouteservice extends Pelias {
constructor(options?: Partial<PeliasOptions>);
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link Openrouteservice}
* @param options the options
*/
export declare function openrouteservice(options?: Partial<PeliasOptions>): Openrouteservice;
/**
* @internal
*/
export type PeliasResponse = GeoJSON.FeatureCollection<GeoJSON.Geometry, Properties> & {
geocoding: Geocoding;
};
interface Properties {
id: string;
layer: string;
source_id: string;
name: string;
confidence: number;
match_type: string;
accuracy: string;
country: string;
country_a: string;
region: string;
region_a: string;
county: string;
county_a: string;
localadmin: string;
locality: string;
continent: string;
label: string;
}
interface Geocoding {
version: string;
attribution: string;
query: Query;
warnings: string[];
engine: Engine;
}
interface Engine {
name: string;
author: string;
version: string;
}
interface Query {
}
export {};

View File

@ -0,0 +1,46 @@
import * as L from 'leaflet';
import { IGeocoder, GeocoderOptions, GeocodingResult } from './api';
export interface PhotonOptions extends GeocoderOptions {
reverseUrl: string;
nameProperties: string[];
htmlTemplate?: (r: any) => string;
}
/**
* Implementation of the [Photon](http://photon.komoot.de/) geocoder
*/
export declare class Photon implements IGeocoder {
options: PhotonOptions;
constructor(options?: Partial<PhotonOptions>);
geocode(query: string): Promise<GeocodingResult[]>;
suggest(query: string): Promise<GeocodingResult[]>;
reverse(latLng: L.LatLngLiteral, scale: number): Promise<GeocodingResult[]>;
_parseResults(data: GeoJSON.FeatureCollection<GeoJSON.Point>): GeocodingResult[];
_decodeFeatureName(f: GeoJSON.Feature): string;
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link Photon}
* @param options the options
*/
export declare function photon(options?: Partial<PhotonOptions>): Photon;
/**
* @internal
*/
export type PhotonResponse = GeoJSON.FeatureCollection<GeoJSON.Geometry, PhotonProperties>;
interface PhotonProperties {
osm_id: number;
osm_type: string;
extent?: number[];
country: string;
osm_key: string;
city: string;
countrycode: string;
osm_value: string;
name: string;
state: string;
type: string;
postcode?: string;
housenumber?: string;
street?: string;
district?: string;
}
export {};

View File

@ -0,0 +1,19 @@
import * as L from 'leaflet';
import { IGeocoder, GeocoderOptions, GeocodingResult } from './api';
export interface What3WordsOptions extends GeocoderOptions {
}
/**
* Implementation of the What3Words service
*/
export declare class What3Words implements IGeocoder {
options: What3WordsOptions;
constructor(options: Partial<What3WordsOptions>);
geocode(query: string): Promise<GeocodingResult[]>;
suggest(query: string): Promise<GeocodingResult[]>;
reverse(location: L.LatLngLiteral, scale: number): Promise<GeocodingResult[]>;
}
/**
* [Class factory method](https://leafletjs.com/reference.html#class-class-factories) for {@link What3Words}
* @param options the options
*/
export declare function what3words(options: Partial<What3WordsOptions>): What3Words;

View File

@ -0,0 +1,5 @@
import { GeocoderControl as Geocoder, geocoder } from './control';
import * as geocoders from './geocoders/index';
import './style.css';
export default Geocoder;
export { Geocoder, geocoder, geocoders };

View File

@ -0,0 +1,12 @@
/**
* @internal
*/
export declare function htmlEscape(string?: string): string;
/**
* @internal
*/
export declare function getJSON<T>(url: string, params: Record<string, unknown>): Promise<T>;
/**
* @internal
*/
export declare function template(str: string, data: Record<string, any>): string;

BIN
public/leaflet-images.zip Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

14419
public/leaflet-src.esm.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

14512
public/leaflet-src.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

661
public/leaflet.css Normal file
View File

@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}

6
public/leaflet.js Normal file

File diff suppressed because one or more lines are too long

1
public/leaflet.js.map Normal file

File diff suppressed because one or more lines are too long

2
public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow:

11
resources/css/app.css Normal file
View File

@ -0,0 +1,11 @@
@import 'tailwindcss';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@source '../**/*.blade.php';
@source '../**/*.js';
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}

1
resources/js/app.js Normal file
View File

@ -0,0 +1 @@
import './bootstrap';

4
resources/js/bootstrap.js vendored Normal file
View File

@ -0,0 +1,4 @@
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

View File

@ -0,0 +1,694 @@
@extends('layouts.app')
@section('title', 'Daftar Laporan Bencana')
@section('page-title', 'Daftar Laporan Bencana')
@section('content')
<div class="container-fluid mt-4">
<div class="card">
<div class="card-body">
<div class="mb-4">
<button class="btn btn-primary" data-toggle="modal" data-target="#addLaporanModal">
<i class="fas fa-plus"></i> Tambah Laporan
</button>
<button class="btn btn-success" data-toggle="modal" data-target="#rekapModal">
<i class="fas fa-file-export"></i> Rekap Laporan
</button>
</div>
<!-- <h5>Daftar Laporan Bencana</h5> -->
<div class="table-responsive">
<table class="table table-hover" id="laporanTable">
<thead class="thead-light">
<tr>
<th>No</th>
<th>Pelapor</th>
<th>Jenis Bencana</th>
<th>Lokasi</th>
<th>Status</th>
<th>Tanggal</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
@foreach($laporanBencana as $index => $laporan)
<tr>
<td>{{ $index + 1 }}</td>
<td>{{ $laporan->pelapor }}</td>
<td>
{{ ucfirst($laporan->jenis_bencana) }}
</td>
<td>
{{ $laporan->lokasi }}
<button class="btn btn-primary btn-sm view-location"
data-lat="{{ explode(',', str_replace(['Lat: ', ' Lng: '], '', $laporan->lokasi))[0] }}"
data-lng="{{ explode(',', str_replace(['Lat: ', ' Lng: '], '', $laporan->lokasi))[1] }}"
data-toggle="tooltip"
title="Lihat Lokasi">
<i class="fas fa-map-marker-alt"></i>
</button>
</td>
<td>
<div class="dropdown">
<button class="btn dropdown-toggle status-btn
{{ $laporan->status === 'menunggu' ? 'btn-warning' :
($laporan->status === 'dalam_proses' ? 'btn-info' : 'btn-success') }}"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
data-id="{{ $laporan->id }}"
style="width: 140px; text-align: left;">
<i class="fas {{
$laporan->status === 'menunggu' ? 'fa-clock' :
($laporan->status === 'dalam_proses' ? 'fa-tools' : 'fa-check-circle')
}} mr-2"></i>
{{ str_replace('_', ' ', ucfirst($laporan->status)) }}
</button>
<div class="dropdown-menu">
<a class="dropdown-item status-item {{ $laporan->status === 'menunggu' ? 'active' : '' }}"
href="#"
data-status="menunggu">
<i class="fas fa-clock text-warning mr-2"></i>
Menunggu
</a>
<a class="dropdown-item status-item {{ $laporan->status === 'dalam_proses' ? 'active' : '' }}"
href="#"
data-status="dalam_proses">
<i class="fas fa-tools text-info mr-2"></i>
Dalam Proses
</a>
<a class="dropdown-item status-item {{ $laporan->status === 'selesai' ? 'active' : '' }}"
href="#"
data-status="selesai">
<i class="fas fa-check-circle text-success mr-2"></i>
Selesai
</a>
</div>
</div>
</td>
<td>{{ \Carbon\Carbon::parse($laporan->created_at)->format('d M Y H:i') }}</td>
<td>
<button class="btn btn-info btn-sm view-foto"
data-foto="{{ $laporan->foto }}"
data-toggle="tooltip"
title="Lihat Foto"
{{ !$laporan->foto ? 'disabled' : '' }}>
<i class="fas fa-image"></i>
</button>
<button class="btn btn-danger btn-sm delete-laporan"
data-id="{{ $laporan->id }}"
data-lokasi="{{ $laporan->lokasi }}"
data-toggle="tooltip"
title="Hapus Laporan">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Modal Tambah Laporan -->
<div class="modal fade" id="addLaporanModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Tambah Laporan Baru</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<form id="addLaporanForm">
<div class="form-group">
<label>Lokasi</label>
<input type="text" class="form-control" name="lokasi" id="lokasi" required readonly>
</div>
<div class="form-group">
<label>Pilih Lokasi di Peta</label>
<div id="map" style="height: 300px;"></div>
</div>
<div class="form-group">
<label>Jenis Bencana</label>
<select class="form-control" name="jenis_bencana" required>
<option value="Banjir">Banjir</option>
<option value="Longsor">Longsor</option>
<option value="Erupsi">Erupsi</option>
<option value="Lahar Panas">Lahar Panas</option>
<option value="Lahar Dingin">Lahar Dingin</option>
<option value="Gempa">Gempa</option>
<option value="Angin Topan">Angin Topan</option>
</select>
</div>
<div class="form-group">
<label>Deskripsi</label>
<textarea class="form-control" name="deskripsi" required></textarea>
</div>
<div class="form-group">
<label>Foto</label>
<input type="file" class="form-control" name="foto">
<small class="text-muted">Opsional. Format gambar, maksimal 5MB.</small>
</div>
<input type="hidden" name="latitude" id="latitude" required>
<input type="hidden" name="longitude" id="longitude" required>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
<button type="button" class="btn btn-primary" id="saveLaporan">Simpan</button>
</div>
</div>
</div>
</div>
<!-- Modal Foto -->
<div class="modal fade" id="fotoModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Foto Bencana</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body text-center">
<img id="fotoPreview" src="" alt="Foto Bencana" class="img-fluid">
</div>
</div>
</div>
</div>
<!-- Modal Konfirmasi Hapus -->
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Konfirmasi Hapus</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<p>Apakah Anda yakin ingin menghapus laporan bencana di lokasi <span id="lokasiText"></span>?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
<button type="button" class="btn btn-danger" id="confirmDelete">Hapus</button>
</div>
</div>
</div>
</div>
<!-- Tambahkan Modal untuk Peta -->
<div class="modal fade" id="locationModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Lokasi Laporan</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<div id="locationMap" style="height: 400px;"></div>
</div>
</div>
</div>
</div>
<!-- Modal Rekap -->
<div class="modal fade" id="rekapModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Rekap Laporan Bencana</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<form id="rekapForm">
<div class="form-group">
<label>Periode</label>
<select class="form-control" name="periode" id="periode" required>
<option value="bulan">Bulanan</option>
<option value="tahun">Tahunan</option>
</select>
</div>
<div class="form-group" id="bulanGroup">
<label>Bulan</label>
<select class="form-control" name="bulan" id="bulan">
<option value="1">Januari</option>
<option value="2">Februari</option>
<option value="3">Maret</option>
<option value="4">April</option>
<option value="5">Mei</option>
<option value="6">Juni</option>
<option value="7">Juli</option>
<option value="8">Agustus</option>
<option value="9">September</option>
<option value="10">Oktober</option>
<option value="11">November</option>
<option value="12">Desember</option>
</select>
</div>
<div class="form-group">
<label>Tahun</label>
<select class="form-control" name="tahun" id="tahun" required>
@for($i = date('Y'); $i >= date('Y')-5; $i--)
<option value="{{ $i }}">{{ $i }}</option>
@endfor
</select>
</div>
<div class="form-group">
<label>Format Export</label>
<select class="form-control" name="format" required>
<option value="pdf">PDF</option>
<option value="excel">Excel</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
<button type="button" class="btn btn-primary" id="exportRekap">Export</button>
</div>
</div>
</div>
</div>
@endsection
@push('styles')
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/1.11.5/css/dataTables.bootstrap4.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/responsive/2.2.9/css/responsive.bootstrap4.min.css">
<style>
.card {
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.table thead th {
background-color: #f8f9fa;
border-bottom: 2px solid #dee2e6;
}
.badge {
padding: 0.5em 0.8em;
font-size: 85%;
}
.btn-sm {
margin: 0 2px;
}
.btn-sm i {
font-size: 0.9rem;
}
/* Status button styles */
.status-btn {
font-size: 0.9rem;
padding: 0.375rem 0.75rem;
color: white !important;
}
.status-btn::after {
float: right;
margin-top: 8px;
}
.dropdown-item {
font-size: 0.9rem;
padding: 0.5rem 1rem;
}
.dropdown-item.active,
.dropdown-item:active {
background-color: #e9ecef;
color: #212529;
}
.dropdown-item:hover {
background-color: #f8f9fa;
}
/* Responsive table styles */
.table-responsive {
padding: 0;
}
#laporanTable {
width: 100% !important;
}
#laporanTable_wrapper .row {
margin: 0;
padding: 15px 0;
}
/* Modal styles */
.modal {
background: rgba(0, 0, 0, 0.5);
}
.modal-backdrop {
display: none;
}
.modal-dialog {
margin: 1.75rem auto;
z-index: 1100;
}
.modal-content {
position: relative;
z-index: 1100;
background: white;
box-shadow: 0 3px 8px rgba(0,0,0,.3);
}
#addAdminModal {
z-index: 1050;
}
#deleteModal {
z-index: 1050;
}
/* Foto Modal Styles */
#fotoModal .modal-dialog {
max-width: 90%;
max-height: 90vh;
margin: 1.75rem auto;
}
#fotoModal .modal-content {
max-height: 90vh;
overflow: hidden;
}
#fotoModal .modal-body {
padding: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
}
#fotoModal .img-fluid {
max-width: 100%;
max-height: 85vh;
object-fit: contain;
}
@media screen and (max-width: 768px) {
.card-body {
padding: 0.5rem;
}
.table td, .table th {
padding: 0.5rem;
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.status-btn {
font-size: 0.8rem;
width: 120px !important;
}
.dropdown-item {
font-size: 0.8rem;
}
#fotoModal .modal-dialog {
max-width: 95%;
margin: 0.5rem auto;
}
#fotoModal .img-fluid {
max-height: 80vh;
}
}
</style>
@endpush
@push('scripts')
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.11.5/js/dataTables.bootstrap4.min.js"></script>
<script src="https://cdn.datatables.net/responsive/2.2.9/js/dataTables.responsive.min.js"></script>
<script src="https://cdn.datatables.net/responsive/2.2.9/js/responsive.bootstrap4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script>
let laporanIdToDelete = null;
let table = null;
let locationMap = null; // Tambahkan variabel global untuk menyimpan instance peta
$(document).ready(function() {
// Inisialisasi tooltip
$('[data-toggle="tooltip"]').tooltip();
// Inisialisasi DataTable
table = $('#laporanTable').DataTable({
responsive: true,
language: {
url: '//cdn.datatables.net/plug-ins/1.11.5/i18n/id.json'
},
columnDefs: [
{ responsivePriority: 1, targets: 0 }, // Nomor
{ responsivePriority: 2, targets: 2 }, // Jenis Bencana
{ responsivePriority: 3, targets: 4 }, // Status
{ responsivePriority: 4, targets: -1 }, // Aksi
{ responsivePriority: 5, targets: 1 }, // Pelapor
{ responsivePriority: 6, targets: 3 }, // Lokasi
{ responsivePriority: 7, targets: 5 } // Tanggal
]
});
// Update Status
$('.status-item').click(function(e) {
e.preventDefault();
const status = $(this).data('status');
const button = $(this).closest('.dropdown').find('.status-btn');
const laporanId = button.data('id');
$.ajax({
url: `/bencana/${laporanId}/status`,
type: 'PUT',
data: {
_token: '{{ csrf_token() }}',
status: status
},
success: function(response) {
if (response.success) {
// Update button appearance
button.removeClass('btn-warning btn-info btn-success');
let newClass = '';
let icon = '';
switch(status) {
case 'menunggu':
newClass = 'btn-warning';
icon = '<i class="fas fa-clock mr-2"></i>';
break;
case 'dalam_proses':
newClass = 'btn-info';
icon = '<i class="fas fa-tools mr-2"></i>';
break;
case 'selesai':
newClass = 'btn-success';
icon = '<i class="fas fa-check-circle mr-2"></i>';
break;
}
button.addClass(newClass);
button.html(icon + status.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()));
} else {
alert(response.message || 'Gagal mengupdate status');
}
},
error: function() {
alert('Terjadi kesalahan saat mengupdate status');
}
});
});
// Lihat foto
$('.view-foto').click(function() {
const foto = $(this).data('foto');
if (foto) {
$('#fotoPreview').attr('src', '/storage/' + foto);
$('#fotoModal').modal('show');
}
});
// Hapus laporan
$('.delete-laporan').click(function() {
laporanIdToDelete = $(this).data('id');
const lokasi = $(this).data('lokasi');
$('#lokasiText').text(lokasi);
$('#deleteModal').modal('show');
});
// Konfirmasi hapus
$('#confirmDelete').click(function() {
if (laporanIdToDelete) {
$.ajax({
url: `/bencana/${laporanIdToDelete}`,
type: 'DELETE',
data: {
_token: '{{ csrf_token() }}'
},
success: function(response) {
if (response.success) {
const row = table.row($(`button[data-id="${laporanIdToDelete}"]`).closest('tr'));
row.remove().draw();
alert('Laporan bencana berhasil dihapus');
} else {
alert('Gagal menghapus laporan bencana');
}
},
error: function() {
alert('Terjadi kesalahan saat menghapus laporan bencana');
}
});
}
$('#deleteModal').modal('hide');
});
// Kirim laporan bencana
$('#saveLaporan').on('click', function(e) {
e.preventDefault();
const form = $('#addLaporanForm');
const formData = new FormData(form[0]);
formData.append('_token', '{{ csrf_token() }}');
$.ajax({
url: '{{ route("bencana.store") }}', // Ganti dengan route yang sesuai
type: 'POST',
data: formData,
contentType: false,
processData: false,
success: function(response) {
if (response.success) {
alert('Laporan berhasil ditambahkan!');
form[0].reset();
$('#addLaporanModal').modal('hide');
location.reload(); // Reload halaman untuk melihat laporan baru
} else {
alert(response.message || 'Gagal menambahkan laporan');
}
},
error: function(xhr) {
alert('Terjadi kesalahan saat menambahkan laporan: ' + xhr.responseJSON.message);
}
});
});
// Resize table
$(window).resize(function() {
table.columns.adjust().responsive.recalc();
});
// Fungsi untuk menampilkan peta lokasi
$('.view-location').click(function() {
const lat = $(this).data('lat');
const lng = $(this).data('lng');
$('#locationModal').modal('show');
// Tunggu modal selesai ditampilkan sebelum menginisialisasi peta
$('#locationModal').on('shown.bs.modal', function () {
// Hapus peta yang ada jika sudah ada
if (locationMap) {
locationMap.remove();
}
// Inisialisasi peta baru
locationMap = L.map('locationMap').setView([lat, lng], 15);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap contributors'
}).addTo(locationMap);
// Tambahkan marker
L.marker([lat, lng]).addTo(locationMap);
// Trigger resize untuk memastikan peta ditampilkan dengan benar
setTimeout(function() {
locationMap.invalidateSize();
}, 100);
});
// Bersihkan peta saat modal ditutup
$('#locationModal').on('hidden.bs.modal', function () {
if (locationMap) {
locationMap.remove();
locationMap = null;
}
});
});
// Handle perubahan periode
$('#periode').change(function() {
if ($(this).val() === 'bulan') {
$('#bulanGroup').show();
} else {
$('#bulanGroup').hide();
}
});
// Export rekap
$('#exportRekap').click(function() {
const form = $('#rekapForm');
const formData = new FormData(form[0]);
formData.append('_token', '{{ csrf_token() }}');
const format = formData.get('format');
const periode = formData.get('periode');
const tahun = formData.get('tahun');
const bulan = formData.get('bulan');
let url = `/bencana/rekap/${format}?tahun=${tahun}`;
if (periode === 'bulan') {
url += `&bulan=${bulan}`;
}
window.location.href = url;
});
});
function initMap() {
var map = L.map('map').setView([-8.132932, 113.221684], 10); // Koordinat awal
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
minZoom: 10.5,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
var marker;
// Event untuk menambahkan marker saat peta diklik
map.on('click', function(e) {
if (marker) {
map.removeLayer(marker); // Hapus marker sebelumnya
}
marker = L.marker(e.latlng).addTo(map); // Tambahkan marker baru
document.getElementById('latitude').value = e.latlng.lat; // Set latitude
document.getElementById('longitude').value = e.latlng.lng; // Set longitude
document.getElementById('lokasi').value = `Lat: ${e.latlng.lat}, Lng: ${e.latlng.lng}`; // Set lokasi
});
}
// Inisialisasi peta saat modal dibuka
$('#addLaporanModal').on('shown.bs.modal', function () {
initMap();
});
</script>
@endpush

View File

@ -0,0 +1,79 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rekap Laporan Bencana</title>
<style>
body {
font-family: Arial, sans-serif;
font-size: 12px;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header h2 {
margin: 0;
padding: 0;
}
.header p {
margin: 5px 0;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
th, td {
border: 1px solid #000;
padding: 5px;
text-align: left;
}
th {
background-color: #f0f0f0;
}
.footer {
text-align: center;
margin-top: 30px;
font-size: 10px;
}
</style>
</head>
<body>
<div class="header">
<h2>Rekap Laporan Bencana</h2>
<p>Periode: {{ $bulan ? \Carbon\Carbon::create()->month($bulan)->format('F') . ' ' . $tahun : $tahun }}</p>
</div>
<table>
<thead>
<tr>
<th>No</th>
<th>Pelapor</th>
<th>Jenis Bencana</th>
<th>Lokasi</th>
<th>Status</th>
<th>Tanggal</th>
<th>Deskripsi</th>
</tr>
</thead>
<tbody>
@foreach($data as $index => $item)
<tr>
<td>{{ $index + 1 }}</td>
<td>{{ $item->pelapor }}</td>
<td>{{ ucfirst($item->jenis_bencana) }}</td>
<td>{{ $item->lokasi }}</td>
<td>{{ str_replace('_', ' ', ucfirst($item->status)) }}</td>
<td>{{ \Carbon\Carbon::parse($item->created_at)->format('d M Y H:i') }}</td>
<td>{{ $item->deskripsi }}</td>
</tr>
@endforeach
</tbody>
</table>
<div class="footer">
<p>Dicetak pada: {{ \Carbon\Carbon::now()->format('d M Y H:i:s') }}</p>
</div>
</body>
</html>

View File

@ -0,0 +1,574 @@
@extends('layouts.admin.app')
@section('title', 'Tambah Cafe')
@section('styles')
@parent
<link href="https://cdn.jsdelivr.net/npm/cropperjs@1.5.13/dist/cropper.min.css" rel="stylesheet">
<style>
#map {
height: 400px !important;
width: 100% !important;
margin-bottom: 15px;
border-radius: 8px;
background-color: #f0f0f0;
border: 1px solid #ccc;
position: relative !important;
z-index: 1 !important;
}
.field-spacer {
margin-bottom: 20px;
}
select {
padding-top: 12px;
padding-bottom: 12px;
padding-left: 6px;
padding-right: 6px;
}
.leaflet-container {
z-index: 1 !important;
}
.leaflet-control-container {
z-index: 1000 !important;
}
/* Crop Modal Styles */
.crop-modal {
display: none;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
overflow: auto;
}
.crop-modal-content {
background-color: #fefefe;
margin: 5% auto;
padding: 20px;
border-radius: 8px;
width: 80%;
max-width: 800px;
}
.crop-img-container {
max-height: 60vh;
overflow: hidden;
margin-bottom: 15px;
}
.crop-controls {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.crop-controls button {
margin-left: 10px;
}
.preview-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 15px;
max-width: 340px;
}
.preview-image {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 4px;
border: 2px solid #ddd;
}
.sortable-indicator {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0,0,0,0.5);
color: white;
z-index: 5;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.sortable-item:hover .sortable-indicator {
opacity: 1;
}
.delete-btn {
position: absolute;
top: 0;
right: 0;
background-color: #ef4444;
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
z-index: 20;
opacity: 0;
transition: all 0.3s;
cursor: pointer;
transform: translate(50%, -50%) scale(0.5);
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
.sortable-item:hover .delete-btn {
opacity: 1;
transform: translate(0, 0) scale(1);
}
.delete-btn:hover {
background-color: #dc2626;
transform: scale(1.1);
}
</style>
@endsection
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Tambah Cafe Baru</h1>
<a href="{{ route('admin.cafes.index') }}" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 cursor-pointer">
Kembali
</a>
</div>
@if ($errors->any())
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6 rounded" role="alert">
<p class="font-bold">Ada kesalahan dalam pengisian form:</p>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form action="{{ route('admin.cafes.store') }}" method="POST" enctype="multipart/form-data" class="space-y-6">
@csrf
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Kolom Kiri: Nama Cafe dan Peta -->
<div class="space-y-6">
<div class="field-spacer">
<label for="nama" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">NAMA CAFE</label>
<input type="text" name="nama" id="nama" value="{{ old('nama') }}" style="min-height: 48px; padding-top: 12px; padding-bottom: 12px; padding-left: 8px;" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" required>
</div>
<div class="field-spacer">
<label for="area" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">AREA</label>
<div class="flex flex-wrap gap-4">
<div class="flex items-center">
<input type="checkbox" name="area[]" id="area_indoor" value="Indoor" {{ is_array(old('area')) && in_array('Indoor', old('area')) ? 'checked' : '' }} class="w-5 h-5 text-blue-600 dark:text-blue-500 rounded border-gray-300 focus:ring-blue-500">
<label for="area_indoor" class="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300">Indoor</label>
</div>
<div class="flex items-center">
<input type="checkbox" name="area[]" id="area_semi_outdoor" value="Semi Outdoor" {{ is_array(old('area')) && in_array('Semi Outdoor', old('area')) ? 'checked' : '' }} class="w-5 h-5 text-blue-600 dark:text-blue-500 rounded border-gray-300 focus:ring-blue-500">
<label for="area_semi_outdoor" class="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300">Semi Outdoor</label>
</div>
<div class="flex items-center">
<input type="checkbox" name="area[]" id="area_outdoor" value="Outdoor" {{ is_array(old('area')) && in_array('Outdoor', old('area')) ? 'checked' : '' }} class="w-5 h-5 text-blue-600 dark:text-blue-500 rounded border-gray-300 focus:ring-blue-500">
<label for="area_outdoor" class="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300">Outdoor</label>
</div>
</div>
</div>
<div class="field-spacer">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">PILIH LOKASI DI PETA</label>
<div id="map"></div>
<p class="text-sm text-gray-500 mt-1">Klik pada peta untuk memilih lokasi cafe. Marker akan muncul dan koordinat akan diisi otomatis.</p>
</div>
</div>
<!-- Kolom Kanan: Semua field lainnya -->
<div class="space-y-6">
<div class="field-spacer">
<label for="koordinat" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">KOORDINAT (LATITUDE, LONGITUDE)</label>
<input type="text" name="koordinat" id="koordinat" value="{{ old('koordinat') }}" placeholder="Contoh: -8.160591, 113.721279" style="min-height: 48px; padding-top: 12px; padding-bottom: 12px; padding-left: 8px;" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50">
<!-- Hidden fields for latitude and longitude -->
<input type="hidden" name="latitude" id="latitude" value="{{ old('latitude') }}">
<input type="hidden" name="longitude" id="longitude" value="{{ old('longitude') }}">
</div>
<div class="field-spacer">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">RENTANG HARGA</label>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="harga_termurah" class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Harga Termurah (Rp)</label>
<input type="text" name="harga_termurah_display" id="harga_termurah_display" value="{{ old('harga_termurah') ? number_format(old('harga_termurah'), 0, ',', '.') : '' }}" placeholder="0" style="min-height: 48px; padding-top: 12px; padding-bottom: 12px; padding-left: 8px;" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50">
<input type="hidden" name="harga_termurah" id="harga_termurah" value="{{ old('harga_termurah') }}">
</div>
<div>
<label for="harga_termahal" class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Harga Termahal (Rp)</label>
<input type="text" name="harga_termahal_display" id="harga_termahal_display" value="{{ old('harga_termahal') ? number_format(old('harga_termahal'), 0, ',', '.') : '' }}" placeholder="0" style="min-height: 48px; padding-top: 12px; padding-bottom: 12px; padding-left: 8px;" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50">
<input type="hidden" name="harga_termahal" id="harga_termahal" value="{{ old('harga_termahal') }}">
</div>
</div>
<div id="harga-error" class="text-red-500 text-sm mt-1" style="display: none;"></div>
<p class="text-sm text-gray-500 mt-1">Masukkan rentang harga menu di cafe ini (opsional)</p>
</div>
@foreach($categories as $category)
<div class="field-spacer">
<label for="category_{{ $category->id }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ strtoupper($category->name) }}</label>
<select name="category_ratings[{{ $category->id }}]" id="category_{{ $category->id }}" style="min-height: 48px;" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 subcategory-select" data-category-id="{{ $category->id }}" required>
<option value="">Pilih {{ $category->name }}</option>
@foreach($category->subcategories as $subcategory)
<option value="{{ $subcategory->id }}" {{ old('category_ratings.'.$category->id) == $subcategory->id ? 'selected' : '' }}>{{ $subcategory->name }}</option>
@endforeach
</select>
</div>
@endforeach
<div class="field-spacer">
<label for="gambar" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">GAMBAR CAFE (OPSIONAL)</label>
<input type="file" name="gambar[]" id="gambar" multiple style="min-height: 48px; padding-top: 12px; padding-bottom: 12px; padding-left: 8px;" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50">
<p class="text-sm text-gray-500 mt-1">Anda dapat memilih lebih dari satu gambar. Format yang didukung: JPG, JPEG, PNG. Maksimal ukuran per file: 2MB</p>
</div>
</div>
</div>
<div class="flex justify-end">
<button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer">
Simpan
</button>
</div>
</form>
</div>
<!-- Crop Modal -->
<div id="cropModal" class="crop-modal">
<div class="crop-modal-content">
<div class="crop-img-container">
<img id="cropImage" src="" alt="Crop Image">
</div>
<div class="crop-controls">
<button id="cropCancel" class="px-4 py-2 bg-gray-400 text-white rounded">Batal</button>
<button id="cropConfirm" class="px-4 py-2 bg-blue-600 text-white rounded">Crop & Simpan</button>
</div>
</div>
</div>
@endsection
@push('scripts')
<!-- Map scripts -->
<script>
$(document).ready(function() {
// Tunggu sebentar untuk memastikan semua elemen sudah dimuat
setTimeout(function() {
const mapContainer = document.getElementById('map');
if (!mapContainer) {
console.error('Map container not found');
return;
}
try {
// Pastikan Leaflet sudah dimuat
if (typeof L === 'undefined') {
console.error('Leaflet library not loaded');
return;
}
console.log('Creating map instance...');
const map = L.map('map', {
center: [-8.159765315131203, 113.72309609838182], // Pusat di Jember sebagai default
zoom: 13,
zoomControl: true,
attributionControl: true,
preferCanvas: true
});
console.log('Adding tile layer...');
const tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
minZoom: 10.5,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});
tileLayer.addTo(map);
let marker;
// Event untuk menambahkan marker saat peta diklik
map.on('click', function(e) {
if (marker) {
map.removeLayer(marker);
}
marker = L.marker(e.latlng).addTo(map);
document.getElementById('latitude').value = e.latlng.lat;
document.getElementById('longitude').value = e.latlng.lng;
document.getElementById('koordinat').value = `${e.latlng.lat}, ${e.latlng.lng}`;
});
// Trigger resize untuk memastikan peta ditampilkan dengan benar
setTimeout(function() {
map.invalidateSize();
console.log('Map size invalidated');
}, 100);
console.log('Map initialized successfully');
} catch (error) {
console.error('Error initializing map:', error);
}
}, 1000);
});
</script>
<!-- Cropper Interactive -->
<script src="https://cdn.jsdelivr.net/npm/cropperjs@1.5.13/dist/cropper.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Variables
var input = document.getElementById('gambar');
var cropModal = document.getElementById('cropModal');
var cropImage = document.getElementById('cropImage');
var cropCancel = document.getElementById('cropCancel');
var cropConfirm = document.getElementById('cropConfirm');
var previewContainer = document.createElement('div');
previewContainer.className = 'preview-container';
input.parentNode.appendChild(previewContainer);
var files = []; // Store files here
var currentIndex = 0;
var cropper = null;
// Create an empty Data Transfer object for storing cropped files
var dt = new DataTransfer();
// Show crop modal for current file
function showCropModal(file) {
var reader = new FileReader();
reader.onload = function(e) {
// Display image in modal
cropImage.src = e.target.result;
cropModal.style.display = 'block';
// Initialize cropper after image is loaded
cropImage.onload = function() {
if (cropper) {
cropper.destroy();
}
// Create new cropper instance with interactive controls
cropper = new Cropper(cropImage, {
aspectRatio: 1, // 1:1 square crop
viewMode: 1, // restrict crop box to not exceed the size of the canvas
dragMode: 'move', // Allow moving the image
guides: true, // Show grid lines
center: true, // Show center indicator
background: false, // Don't show grey background
autoCropArea: 0.8, // Define default crop area size (80% of the image)
cropBoxMovable: true,
cropBoxResizable: true,
toggleDragModeOnDblclick: true
});
};
};
reader.readAsDataURL(file);
}
// Process the current file
function processCurrentFile() {
if (currentIndex < files.length) {
showCropModal(files[currentIndex]);
} else {
// All files processed, update the input
input.files = dt.files;
}
}
// File selection handler
input.addEventListener('change', function(e) {
// Clear previous state
while(previewContainer.firstChild) {
previewContainer.removeChild(previewContainer.firstChild);
}
if (this.files.length === 0) return;
// Reset variables
files = Array.from(this.files);
currentIndex = 0;
dt = new DataTransfer();
// Start processing the first file
processCurrentFile();
});
// Cancel button handler
cropCancel.addEventListener('click', function() {
cropModal.style.display = 'none';
if (cropper) {
cropper.destroy();
cropper = null;
}
input.value = '';
files = [];
currentIndex = 0;
dt = new DataTransfer();
while(previewContainer.firstChild) {
previewContainer.removeChild(previewContainer.firstChild);
}
});
// Confirm button handler
cropConfirm.addEventListener('click', function() {
if (!cropper) return;
// Get cropped canvas
cropper.getCroppedCanvas({
width: 600, // output size
height: 600,
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high'
}).toBlob(function(blob) {
// Create a new file from the blob
var croppedFile = new File([blob], files[currentIndex].name, {
type: 'image/jpeg',
lastModified: new Date().getTime()
});
// Add to data transfer object
dt.items.add(croppedFile);
// Create preview container with delete button
var previewItem = document.createElement('div');
previewItem.className = 'relative group sortable-item';
previewItem.style.display = 'inline-block';
// Create preview image
var preview = document.createElement('img');
preview.src = URL.createObjectURL(blob);
preview.className = 'preview-image';
// Create delete button
var deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-btn';
deleteBtn.title = 'Hapus gambar';
deleteBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg>';
// Add click event to delete button
deleteBtn.addEventListener('click', function() {
const index = Array.from(previewContainer.children).indexOf(previewItem);
if (index !== -1) {
// Create new DataTransfer to rebuild file list without this item
const newDt = new DataTransfer();
for (let i = 0; i < dt.files.length; i++) {
if (i !== index) {
newDt.items.add(dt.files[i]);
}
}
dt = newDt;
input.files = dt.files;
// Remove visual preview
previewItem.remove();
}
});
// Append elements to preview item
previewItem.appendChild(preview);
previewItem.appendChild(deleteBtn);
// Add to preview container
previewContainer.appendChild(previewItem);
// Hide modal and cleanup
cropModal.style.display = 'none';
if (cropper) {
cropper.destroy();
cropper = null;
}
// Move to next file
currentIndex++;
processCurrentFile();
}, 'image/jpeg', 0.95);
});
});
</script>
<!-- Price formatting and validation script -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const hargaTermurahDisplay = document.getElementById('harga_termurah_display');
const hargaTermahalDisplay = document.getElementById('harga_termahal_display');
const hargaTermurah = document.getElementById('harga_termurah');
const hargaTermahal = document.getElementById('harga_termahal');
const hargaError = document.getElementById('harga-error');
const form = document.querySelector('form');
// Format number with thousands separator
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
}
// Remove thousands separator and get numeric value
function unformatNumber(str) {
return parseInt(str.replace(/\./g, '')) || 0;
}
// Format input on keyup
function handleNumberInput(displayInput, hiddenInput) {
displayInput.addEventListener('input', function() {
let value = this.value.replace(/[^\d]/g, ''); // Only allow digits
if (value) {
this.value = formatNumber(value);
hiddenInput.value = value;
} else {
this.value = '';
hiddenInput.value = '';
}
validatePrices();
});
}
// Validate that harga_termahal >= harga_termurah
function validatePrices() {
const termurah = unformatNumber(hargaTermurahDisplay.value);
const termahal = unformatNumber(hargaTermahalDisplay.value);
if (termurah > 0 && termahal > 0 && termahal < termurah) {
hargaError.textContent = 'Harga termahal harus lebih besar atau sama dengan harga termurah';
hargaError.style.display = 'block';
hargaTermahalDisplay.classList.add('border-red-500');
hargaTermurahDisplay.classList.add('border-red-500');
return false;
} else {
hargaError.style.display = 'none';
hargaTermahalDisplay.classList.remove('border-red-500');
hargaTermurahDisplay.classList.remove('border-red-500');
return true;
}
}
// Initialize formatting
handleNumberInput(hargaTermurahDisplay, hargaTermurah);
handleNumberInput(hargaTermahalDisplay, hargaTermahal);
// Prevent form submission if validation fails
form.addEventListener('submit', function(e) {
if (!validatePrices()) {
e.preventDefault();
hargaError.scrollIntoView({ behavior: 'smooth' });
}
});
});
</script>
@endpush

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,117 @@
@extends('layouts.admin.app')
@section('title', '| Daftar Cafe')
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Daftar Cafe</h1>
<a href="{{ route('admin.cafes.create') }}" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer">
Tambah
</a>
</div>
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="px-6 py-3">NAMA CAFE</th>
<th scope="col" class="px-6 py-3">HARGA</th>
<th scope="col" class="px-6 py-3">JARAK DENGAN PUSAT KOTA</th>
<th scope="col" class="px-6 py-3">JAM OPERASIONAL</th>
<th scope="col" class="px-6 py-3">FOTOGENIK</th>
<th scope="col" class="px-6 py-3">AREA</th>
<th scope="col" class="px-6 py-3">FASILITAS</th>
<th scope="col" class="px-6 py-3 text-right">AKSI</th>
</tr>
</thead>
<tbody>
@forelse($cafes as $cafe)
<tr class="bg-white dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600">
<td class="px-6 py-4 font-medium text-gray-900 dark:text-white">
{{ $cafe->nama }}
</td>
<td class="px-6 py-4 text-gray-500 dark:text-gray-400">
@php
$hargaRating = $cafe->harga;
@endphp
{{ $hargaRating ? $hargaRating->name : '-' }}
</td>
<td class="px-6 py-4 text-gray-500 dark:text-gray-400">
@php
$jarakRating = $cafe->jarakKampus;
@endphp
{{ $jarakRating ? $jarakRating->name : '-' }}
</td>
<td class="px-6 py-4 text-gray-500 dark:text-gray-400">
@php
$jamOperasionalCategory = \App\Models\Category::where('name', 'Jam Operasional')->first();
$jamOperasionalRating = $jamOperasionalCategory ? $cafe->getCategoryRating($jamOperasionalCategory->id) : null;
@endphp
{{ $jamOperasionalRating ? $jamOperasionalRating->name : '-' }}
</td>
<td class="px-6 py-4 text-gray-500 dark:text-gray-400">
@php
$fotogenikCategory = \App\Models\Category::where('name', 'Fotogenik')->first();
$fotogenikRating = $fotogenikCategory ? $cafe->getCategoryRating($fotogenikCategory->id) : null;
@endphp
{{ $fotogenikRating ? $fotogenikRating->name : '-' }}
</td>
<td class="px-6 py-4 text-gray-500 dark:text-gray-400">
{{ $cafe->area ?? '-' }}
</td>
<td class="px-6 py-4 text-gray-500 dark:text-gray-400">
@php
$fasilitasCategory = \App\Models\Category::where('name', 'Fasilitas')->first();
$fasilitasRating = $fasilitasCategory ? $cafe->getCategoryRating($fasilitasCategory->id) : null;
@endphp
{{ $fasilitasRating ? $fasilitasRating->name : '-' }}
</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end space-x-4">
<a href="{{ route('admin.cafes.edit', $cafe->id) }}?ts={{ time() }}" class="text-xs font-semibold uppercase hover:underline text-blue-600 dark:text-blue-400">
Edit
</a>
<form action="{{ route('admin.cafes.destroy', $cafe) }}" method="POST" onsubmit="return confirm('Apakah Anda yakin ingin menghapus cafe ini?');" class="inline">
@csrf
@method('DELETE')
<button type="submit" class="text-xs font-semibold uppercase hover:underline cursor-pointer text-blue-600 dark:text-blue-400">
Hapus
</button>
</form>
</div>
</td>
</tr>
@empty
<tr class="bg-white dark:bg-gray-700">
<td colspan="9" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400">
Tidak ada cafe yang ditemukan.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-6">
{{ $cafes->links() }}
</div>
</div>
<style>
@media (max-width: 768px) {
.overflow-x-auto {
-webkit-overflow-scrolling: touch;
}
table {
min-width: 100%;
}
th, td {
padding: 0.5rem;
font-size: 0.875rem;
}
}
</style>
@endsection

View File

@ -0,0 +1,100 @@
@extends('layouts.admin.app')
@section('title', '| Detail Cafe')
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Detail Cafe: {{ $cafe->nama }}</h1>
<div class="flex space-x-2">
<a href="{{ route('admin.cafes.edit', $cafe) }}" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer">
Edit
</a>
<a href="{{ route('admin.cafes.index') }}" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 cursor-pointer">
Kembali
</a>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="md:col-span-2 space-y-6">
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-6">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Informasi Cafe</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Nama</p>
<p class="text-lg font-medium text-gray-900 dark:text-white">{{ $cafe->nama }}</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Lokasi</p>
<p class="text-lg font-medium text-gray-900 dark:text-white">{{ $cafe->lokasi }}</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Area</p>
<p class="text-lg font-medium text-gray-900 dark:text-white">{{ $cafe->area ?? 'Tidak ada data' }}</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Jarak dari Kampus</p>
<p class="text-lg font-medium text-gray-900 dark:text-white">{{ $cafe->jarak_kampus }}</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Kisaran Harga</p>
<p class="text-lg font-medium text-gray-900 dark:text-white">{{ $cafe->harga }}</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">WiFi</p>
<div class="mt-1">
<span class="inline-flex text-xs leading-5 font-semibold rounded-full px-2 py-1 {{ $cafe->wifi ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
{{ $cafe->wifi ? 'Tersedia' : 'Tidak Tersedia' }}
</span>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-6">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Fasilitas</h2>
<div class="flex flex-wrap gap-2">
@foreach(explode(',', $cafe->fasilitas) as $fasilitas)
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100">
{{ trim($fasilitas) }}
</span>
@endforeach
</div>
</div>
</div>
<div class="md:col-span-1">
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-6">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Foto Cafe</h2>
@if($cafe->gambar)
<img src="{{ asset($cafe->gambar) }}" alt="{{ $cafe->nama }}" class="w-full h-auto rounded-lg">
@else
<div class="w-full h-48 bg-gray-200 dark:bg-gray-600 rounded-lg flex items-center justify-center">
<p class="text-gray-500 dark:text-gray-400">Tidak ada foto</p>
</div>
@endif
</div>
<div class="mt-6 bg-gray-50 dark:bg-gray-700 rounded-lg p-6">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Informasi Lainnya</h2>
<div class="space-y-2">
<p class="text-sm text-gray-500 dark:text-gray-400">Dibuat pada</p>
<p class="text-md font-medium text-gray-900 dark:text-white">{{ $cafe->created_at->format('d M Y, H:i') }}</p>
</div>
<div class="space-y-2 mt-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Terakhir diperbarui</p>
<p class="text-md font-medium text-gray-900 dark:text-white">{{ $cafe->updated_at->format('d M Y, H:i') }}</p>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,31 @@
@extends('layouts.admin.app')
@section('title', '| Tambah Kategori')
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Tambah Kriteria</h1>
<a href="{{ route('admin.categories.index') }}" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 cursor-pointer">
Kembali
</a>
</div>
<form action="{{ route('admin.categories.store') }}" method="POST" class="space-y-6">
@csrf
<div class="space-y-6">
<div class="field-spacer">
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">NAMA KRITERIA</label>
<input type="text" name="name" id="name" value="{{ old('name') }}" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" required>
</div>
</div>
<div class="flex justify-end">
<button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer">
Simpan
</button>
</div>
</form>
</div>
@endsection

View File

@ -0,0 +1,32 @@
@extends('layouts.admin.app')
@section('title', '| Edit Kategori')
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Edit Kriteria</h1>
<a href="{{ route('admin.categories.index') }}" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 cursor-pointer">
Kembali
</a>
</div>
<form action="{{ route('admin.categories.update', $category->id) }}" method="POST" class="space-y-6">
@csrf
@method('PUT')
<div class="space-y-6">
<div class="field-spacer">
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">NAMA KRITERIA</label>
<input type="text" name="name" id="name" value="{{ old('name', $category->name) }}" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" required>
</div>
</div>
<div class="flex justify-end">
<button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer">
Simpan
</button>
</div>
</form>
</div>
@endsection

View File

@ -0,0 +1,64 @@
@extends('layouts.admin.app')
@section('title', '| Daftar Kategori')
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Daftar Kriteria</h1>
<a href="{{ route('admin.categories.create') }}" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer">
Tambah
</a>
</div>
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 overflow-hidden rounded-lg">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr class="h-12">
<th scope="col" class="px-6 py-3 rounded-tl-lg">
NO
</th>
<th scope="col" class="px-6 py-3">
NAMA KRITERIA
</th>
<th scope="col" class="px-6 py-3 text-right rounded-tr-lg">
AKSI
</th>
</tr>
</thead>
<tbody>
@forelse($categories as $index => $category)
<tr class="h-16 bg-white dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600 transition-colors hover:bg-gray-50 dark:hover:bg-gray-600">
<th scope="row" class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap {{ $loop->first ? 'rounded-tl-lg' : '' }} {{ $loop->last ? 'rounded-bl-lg' : '' }}">
{{ $index + 1 }}
</th>
<td class="px-6 py-4 text-gray-500 dark:text-gray-400">
{{ $category->name }}
</td>
<td class="px-6 py-4 text-right {{ $loop->first ? 'rounded-tr-lg' : '' }} {{ $loop->last ? 'rounded-br-lg' : '' }}">
<div class="flex items-center justify-end space-x-4">
<a href="{{ route('admin.categories.edit', $category) }}" class="text-xs font-semibold uppercase hover:underline text-blue-600 dark:text-blue-400">
Edit
</a>
<form action="{{ route('admin.categories.destroy', $category) }}" method="POST" onsubmit="return confirm('Apakah Anda yakin ingin menghapus kategori ini?');" class="inline">
@csrf
@method('DELETE')
<button type="submit" class="text-xs font-semibold uppercase hover:underline cursor-pointer text-blue-600 dark:text-blue-400">
Hapus
</button>
</form>
</div>
</td>
</tr>
@empty
<tr class="h-16 bg-white dark:bg-gray-700">
<td colspan="3" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400 rounded-b-lg">
Tidak ada kategori yang ditemukan.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
@endsection

View File

@ -0,0 +1,91 @@
@extends('layouts.admin.app')
@section('title', '| Pesan Kontak')
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Daftar Pesan Kontak</h1>
</div>
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 overflow-hidden rounded-lg">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr class="h-12">
<th scope="col" class="px-6 py-3 rounded-tl-lg">
NAMA
</th>
<th scope="col" class="px-6 py-3">
EMAIL
</th>
<th scope="col" class="px-6 py-3">
PESAN
</th>
<th scope="col" class="px-6 py-3">
TANGGAL
</th>
<th scope="col" class="px-6 py-3">
STATUS
</th>
<th scope="col" class="px-6 py-3 text-right rounded-tr-lg">
</th>
</tr>
</thead>
<tbody>
@forelse($messages as $message)
<tr class="h-16 bg-white dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600 transition-colors hover:bg-gray-50 dark:hover:bg-gray-600">
<th scope="row" class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap">
{{ $message->name }}
</th>
<td class="px-6 py-4 text-gray-500 dark:text-gray-400">
{{ $message->email }}
</td>
<td class="px-6 py-4 text-gray-500 dark:text-gray-400">
{{ Str::limit($message->message, 50) }}
</td>
<td class="px-6 py-4 text-gray-500 dark:text-gray-400">
{{ $message->created_at->format('d M Y H:i') }}
</td>
<td class="px-6 py-4">
@if($message->is_read)
<span class="inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
Dibaca
</span>
@else
<span class="inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">
Belum Dibaca
</span>
@endif
</td>
<td class="px-6 py-4 text-right {{ $loop->last && $loop->first ? 'rounded-br-lg' : '' }} {{ $loop->last ? 'rounded-br-lg' : '' }}">
<div class="flex items-center justify-end space-x-4">
<a href="{{ route('admin.contact-messages.show', $message) }}" class="text-xs font-semibold uppercase hover:underline text-blue-600 dark:text-blue-400">
Detail
</a>
<form action="{{ route('admin.contact-messages.destroy', $message) }}" method="POST" onsubmit="return confirm('Apakah Anda yakin ingin menghapus pesan ini?');" class="inline">
@csrf
@method('DELETE')
<button type="submit" class="text-xs font-semibold uppercase hover:underline text-blue-600 dark:text-blue-400 cursor-pointer">
Hapus
</button>
</form>
</div>
</td>
</tr>
@empty
<tr class="h-16 bg-white dark:bg-gray-700">
<td colspan="6" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400 rounded-b-lg">
Tidak ada pesan kontak yang ditemukan.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-6">
{{ $messages->links() }}
</div>
</div>
@endsection

View File

@ -0,0 +1,108 @@
@extends('layouts.admin.app')
@section('title', '| Detail Pesan Kontak')
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Detail Pesan Kontak</h1>
<div>
<a href="{{ route('admin.contact-messages.index') }}" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 transition-colors duration-200 inline-flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
</svg>
Kembali
</a>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-gray-50 dark:bg-gray-700 p-6 rounded-lg border border-gray-200 dark:border-gray-600">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Informasi Pengirim</h2>
<div class="mb-4">
<p class="text-sm text-gray-500 dark:text-gray-400">ID</p>
<p class="text-lg text-gray-800 dark:text-white">{{ $contactMessage->id }}</p>
</div>
<div class="mb-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Nama</p>
<p class="text-lg text-gray-800 dark:text-white">{{ $contactMessage->name }}</p>
</div>
<div class="mb-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Email</p>
<p class="text-lg text-gray-800 dark:text-white">{{ $contactMessage->email }}</p>
</div>
<div class="mb-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Status</p>
<p class="text-lg text-gray-800 dark:text-white">
@if($contactMessage->is_read)
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
Sudah Dibaca
</span>
@else
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
Belum Dibaca
</span>
@endif
</p>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 p-6 rounded-lg border border-gray-200 dark:border-gray-600">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Detail Pesan</h2>
<div class="mb-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Tanggal Dikirim</p>
<p class="text-lg text-gray-800 dark:text-white">{{ $contactMessage->created_at->format('d M Y H:i') }}</p>
</div>
<div class="mb-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Terakhir Diupdate</p>
<p class="text-lg text-gray-800 dark:text-white">{{ $contactMessage->updated_at->format('d M Y H:i') }}</p>
</div>
</div>
</div>
<div class="mt-6 bg-gray-50 dark:bg-gray-700 p-6 rounded-lg border border-gray-200 dark:border-gray-600">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Isi Pesan</h2>
<div class="p-4 bg-white dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600">
<p class="text-gray-800 dark:text-white whitespace-pre-wrap">{{ $contactMessage->message }}</p>
</div>
</div>
<div class="mt-6 border-t border-gray-200 dark:border-gray-600 pt-6">
<div class="flex space-x-4">
<a href="mailto:{{ $contactMessage->email }}?subject=Re: Pesan dari Website RECAJE&body=Halo {{ $contactMessage->name }}, %0D%0A%0D%0ATerima kasih telah menghubungi kami. Berikut adalah balasan atas pesan Anda:%0D%0A%0D%0A%0D%0A%0D%0A--------------------%0D%0APesan asli dari {{ $contactMessage->name }} pada {{ $contactMessage->created_at->format('d M Y H:i') }}:%0D%0A%0D%0A{{ $contactMessage->message }}" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors duration-200 inline-flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
Balas Pesan
</a>
@if(!$contactMessage->is_read)
<form action="{{ route('admin.contact-messages.mark-as-read', $contactMessage->id) }}" method="POST">
@csrf
@method('PATCH')
<button type="submit" class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200 inline-flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Tandai Sudah Dibaca
</button>
</form>
@endif
<form action="{{ route('admin.contact-messages.destroy', $contactMessage) }}" method="POST" onsubmit="return confirm('Apakah Anda yakin ingin menghapus pesan ini? Tindakan ini tidak dapat dibatalkan.');">
@csrf
@method('DELETE')
<button type="submit" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-md transition-colors duration-200">
Hapus Pesan
</button>
</form>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,114 @@
@extends('layouts.admin.app')
@section('title', ' - Dashboard')
@section('content')
<div>
<h1 class="text-2xl font-semibold text-gray-800 dark:text-gray-800 mb-4">Dashboard Admin</h1>
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<a href="{{ route('admin.users.index') }}" class="block">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 border border-gray-100 dark:border-gray-700 hover:shadow-lg transition-shadow">
<div class="flex items-center space-x-4">
<div class="bg-blue-500 rounded-md p-3 flex-shrink-0">
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
</div>
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Total Pengguna</p>
<p class="text-2xl font-semibold text-gray-800 dark:text-white">{{ $totalUsers }}</p>
</div>
</div>
</div>
</a>
<a href="{{ route('admin.cafes.index') }}" class="block">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 border border-gray-100 dark:border-gray-700 hover:shadow-lg transition-shadow">
<div class="flex items-center space-x-4">
<div class="bg-green-500 rounded-md p-3 flex-shrink-0">
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"></path>
</svg>
</div>
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Total Café</p>
<p class="text-2xl font-semibold text-gray-800 dark:text-white">{{ \App\Models\Cafe::count() }}</p>
</div>
</div>
</div>
</a>
<a href="{{ route('admin.contact-messages.index') }}" class="block">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 border border-gray-100 dark:border-gray-700 hover:shadow-lg transition-shadow">
<div class="flex items-center space-x-4">
<div class="bg-purple-500 rounded-md p-3 flex-shrink-0">
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
</div>
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Pesan Belum Dibaca</p>
<p class="text-2xl font-semibold text-gray-800 dark:text-white">{{ \App\Models\ContactMessage::where('is_read', false)->count() }}</p>
</div>
</div>
</div>
</a>
</div>
<!-- Recent Users -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Pengguna Terbaru</h2>
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 overflow-hidden rounded-lg">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr class="h-12">
<th scope="col" class="px-6 py-3 rounded-tl-lg">
NAMA
</th>
<th scope="col" class="px-6 py-3">
EMAIL
</th>
<th scope="col" class="px-6 py-3">
ROLE
</th>
<th scope="col" class="px-6 py-3 rounded-tr-lg">
TANGGAL DAFTAR
</th>
</tr>
</thead>
<tbody>
@php
$recentUsers = \App\Models\User::where('role', 'user')->latest()->take(5)->get();
@endphp
@forelse($recentUsers as $user)
<tr class="h-16 bg-white dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600 transition-colors hover:bg-gray-50 dark:hover:bg-gray-600">
<th scope="row" class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap">
{{ $user->name }}
</th>
<td class="px-6 py-4 text-gray-500 dark:text-gray-400">
{{ $user->email }}
</td>
<td class="px-6 py-4">
<span class="inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
{{ ucfirst($user->role) }}
</span>
</td>
<td class="px-6 py-4 text-gray-500 dark:text-gray-400">
{{ $user->created_at->format('d M Y') }}
</td>
</tr>
@empty
<tr class="h-16 bg-white dark:bg-gray-700">
<td colspan="4" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400 rounded-b-lg">
Tidak ada pengguna yang ditemukan.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,153 @@
@extends('layouts.admin.app')
@section('title', '| Tambah Sub-Kategori')
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Tambah Sub-Kriteria</h1>
<a href="{{ route('admin.subcategories.index') }}" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 cursor-pointer">
Kembali
</a>
</div>
<form action="{{ route('admin.subcategories.store') }}" method="POST" class="space-y-6" id="subcategory-form">
@csrf
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-6">
<div class="field-spacer">
<label for="category_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">KRITERIA</label>
<select name="category_id" id="category_id" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" required>
<option value="">Pilih Kriteria</option>
@foreach($categories as $category)
<option value="{{ $category->id }}" {{ old('category_id', $selectedCategoryId) == $category->id ? 'selected' : '' }}>
{{ $category->name }}
</option>
@endforeach
</select>
</div>
</div>
</div>
<div class="space-y-6">
<div class="field-spacer">
<div id="subcategory-container" class="space-y-6">
<div class="subcategory-item p-4 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">NAMA SUB-KRITERIA</label>
<input type="text" name="subcategories[0][name]" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" placeholder="Masukkan nama sub-kategori" required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">NILAI SMART</label>
<input type="number" name="subcategories[0][value]" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" placeholder="1-5" min="1" max="5" required>
</div>
</div>
<div class="mt-4 flex justify-end">
<button type="button" class="remove-subcategory px-3 py-1.5 bg-red-600 text-white text-sm rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50" style="display: none;">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline-block mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Hapus
</button>
</div>
</div>
</div>
<div class="mt-4">
<button type="button" id="add-subcategory" class="inline-flex items-center px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Tambah
</button>
</div>
</div>
</div>
<div class="flex justify-end pt-4 border-t border-gray-200 dark:border-gray-700">
<button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
Simpan
</button>
</div>
</form>
</div>
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM loaded, initializing subcategory form');
const container = document.getElementById('subcategory-container');
const addButton = document.getElementById('add-subcategory');
const form = document.getElementById('subcategory-form');
let subcategoryCount = 1;
console.log('Container:', container);
console.log('Add button:', addButton);
console.log('Form:', form);
// Tampilkan tombol hapus pada item pertama jika ada lebih dari satu item
updateRemoveButtons();
addButton.addEventListener('click', function() {
console.log('Add button clicked, adding new subcategory');
const item = document.createElement('div');
item.className = 'subcategory-item p-4 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg';
item.innerHTML = `
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">NAMA SUB-KATEGORI</label>
<input type="text" name="subcategories[${subcategoryCount}][name]" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" placeholder="Masukkan nama sub-kategori" required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">NILAI SMART</label>
<input type="number" name="subcategories[${subcategoryCount}][value]" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" placeholder="1-5" min="1" max="5" required>
</div>
</div>
<div class="mt-4 flex justify-end">
<button type="button" class="remove-subcategory px-3 py-1.5 bg-red-600 text-white text-sm rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 inline-block mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Hapus
</button>
</div>
`;
container.appendChild(item);
subcategoryCount++;
updateRemoveButtons();
console.log('New subcategory added, count:', subcategoryCount);
});
container.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-subcategory') || e.target.closest('.remove-subcategory')) {
console.log('Remove button clicked');
e.target.closest('.subcategory-item').remove();
updateRemoveButtons();
}
});
form.addEventListener('submit', function(e) {
console.log('Form submitted');
const formData = new FormData(form);
console.log('Form data:');
for (let pair of formData.entries()) {
console.log(pair[0] + ': ' + pair[1]);
}
});
function updateRemoveButtons() {
const items = container.querySelectorAll('.subcategory-item');
const removeButtons = container.querySelectorAll('.remove-subcategory');
console.log('Updating remove buttons, items count:', items.length);
// Tampilkan tombol hapus jika ada lebih dari satu item
removeButtons.forEach(button => {
button.style.display = items.length > 1 ? 'inline-flex' : 'none';
});
}
});
</script>
@endpush
@endsection

View File

@ -0,0 +1,53 @@
@extends('layouts.admin.app')
@section('title', '| Edit Sub-Kategori')
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Edit Sub-Kriteria</h1>
<a href="{{ route('admin.subcategories.index') }}" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 cursor-pointer">
Kembali
</a>
</div>
<form action="{{ route('admin.subcategories.update', $subcategory) }}" method="POST" class="space-y-6">
@csrf
@method('PUT')
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-6">
<div class="field-spacer">
<label for="category_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">KRITERIA</label>
<select name="category_id" id="category_id" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" required>
<option value="">Pilih Kriteria</option>
@foreach($categories as $category)
<option value="{{ $category->id }}" {{ old('category_id', $subcategory->category_id) == $category->id ? 'selected' : '' }}>
{{ $category->name }}
</option>
@endforeach
</select>
</div>
</div>
<div class="space-y-6">
<div class="field-spacer">
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">NAMA SUB-KRITERIA</label>
<input type="text" name="name" id="name" value="{{ old('name', $subcategory->name) }}" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" required>
</div>
<div class="field-spacer">
<label for="value" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">NILAI SMART</label>
<input type="number" name="value" id="value" value="{{ old('value', $subcategory->value) }}" min="1" max="5" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" required placeholder="1-5">
</div>
</div>
</div>
<div class="flex justify-end">
<button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer">
Simpan
</button>
</div>
</form>
</div>
@endsection

View File

@ -0,0 +1,80 @@
@extends('layouts.admin.app')
@section('title', '| Sub-Kategori')
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Daftar Sub-Kriteria</h1>
<a href="{{ route('admin.subcategories.create') }}" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer">
Tambah
</a>
</div>
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 overflow-hidden rounded-lg">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr class="h-12">
<th scope="col" class="px-6 py-3 rounded-tl-lg">
NO
</th>
<th scope="col" class="px-6 py-3">
NAMA SUB-KRITERIA
</th>
<th scope="col" class="px-6 py-3">
KRITERIA
</th>
<th scope="col" class="px-6 py-3">
NILAI SMART
</th>
<th scope="col" class="px-6 py-3 text-right rounded-tr-lg">
AKSI
</th>
</tr>
</thead>
<tbody>
@forelse($subcategories as $index => $subcategory)
<tr class="h-16 bg-white dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600 transition-colors hover:bg-gray-50 dark:hover:bg-gray-600">
<th scope="row" class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap {{ $loop->first ? 'rounded-tl-lg' : '' }} {{ $loop->last ? 'rounded-bl-lg' : '' }}">
{{ $index + 1 }}
</th>
<td class="px-6 py-4 text-gray-500 dark:text-gray-400 select-text">
{{ $subcategory->name }}
</td>
<td class="px-6 py-4 text-gray-500 dark:text-gray-400">
{{ $subcategory->category->name }}
</td>
<td class="px-6 py-4 text-gray-500 dark:text-gray-400">
{{ $subcategory->value ?? '-' }}
</td>
<td class="px-6 py-4 text-right {{ $loop->first ? 'rounded-tr-lg' : '' }} {{ $loop->last ? 'rounded-br-lg' : '' }}">
<div class="flex items-center justify-end space-x-4">
<a href="{{ route('admin.subcategories.edit', $subcategory) }}" class="text-xs font-semibold uppercase hover:underline text-blue-600 dark:text-blue-400">
Edit
</a>
<form action="{{ route('admin.subcategories.destroy', $subcategory) }}" method="POST" onsubmit="return confirm('Apakah Anda yakin ingin menghapus sub-kategori ini?');" class="inline">
@csrf
@method('DELETE')
<button type="submit" class="text-xs font-semibold uppercase hover:underline cursor-pointer text-blue-600 dark:text-blue-400">
Hapus
</button>
</form>
</div>
</td>
</tr>
@empty
<tr class="h-16 bg-white dark:bg-gray-700">
<td colspan="5" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400 rounded-b-lg">
Tidak ada sub-kategori yang ditemukan.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-6">
{{ $subcategories->links() }}
</div>
</div>
@endsection

View File

@ -0,0 +1,82 @@
@extends('layouts.admin.app')
@section('title', '| Tambah Pengguna')
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Tambah Pengguna</h1>
<div>
<a href="{{ route('admin.users.index') }}" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 transition-colors duration-200 inline-flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
</svg>
Kembali
</a>
</div>
</div>
<form action="{{ route('admin.users.store') }}" method="POST" class="space-y-6">
@csrf
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Kolom Kiri -->
<div class="space-y-6">
<div class="field-spacer">
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">NAMA</label>
<input type="text" name="name" id="name" value="{{ old('name') }}" required
class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" style="min-height: 48px;">
@error('name')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="field-spacer">
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">EMAIL</label>
<input type="email" name="email" id="email" value="{{ old('email') }}" required
class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" style="min-height: 48px;">
@error('email')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
</div>
<!-- Kolom Kanan -->
<div class="space-y-6">
<div class="field-spacer">
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">PASSWORD</label>
<input type="password" name="password" id="password" required
class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" style="min-height: 48px;">
@error('password')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="field-spacer">
<label for="password_confirmation" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">KONFIRMASI PASSWORD</label>
<input type="password" name="password_confirmation" id="password_confirmation" required
class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" style="min-height: 48px;">
</div>
<div class="field-spacer">
<label for="role" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">ROLE</label>
<select name="role" id="role" required
class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" style="min-height: 48px;">
<option value="user" {{ old('role') == 'user' ? 'selected' : '' }}>User</option>
<option value="admin" {{ old('role') == 'admin' ? 'selected' : '' }}>Admin</option>
</select>
@error('role')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
</div>
</div>
<div class="flex justify-end">
<button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer">
Simpan
</button>
</div>
</form>
</div>
@endsection

View File

@ -0,0 +1,83 @@
@extends('layouts.admin.app')
@section('title', '| Edit Pengguna')
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Edit Pengguna</h1>
<div>
<a href="{{ route('admin.users.index') }}" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 transition-colors duration-200 inline-flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
</svg>
Kembali
</a>
</div>
</div>
<form action="{{ route('admin.users.update', $user) }}" method="POST" class="space-y-6">
@csrf
@method('PUT')
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Kolom Kiri -->
<div class="space-y-6">
<div class="field-spacer">
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">NAMA</label>
<input type="text" name="name" id="name" value="{{ old('name', $user->name) }}" required
class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" style="min-height: 48px;">
@error('name')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="field-spacer">
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">EMAIL</label>
<input type="email" name="email" id="email" value="{{ old('email', $user->email) }}" required
class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" style="min-height: 48px;">
@error('email')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
</div>
<!-- Kolom Kanan -->
<div class="space-y-6">
<div class="field-spacer">
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">PASSWORD BARU (KOSONGKAN JIKA TIDAK INGIN MENGUBAH)</label>
<input type="password" name="password" id="password"
class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" style="min-height: 48px;">
@error('password')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="field-spacer">
<label for="password_confirmation" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">KONFIRMASI PASSWORD BARU</label>
<input type="password" name="password_confirmation" id="password_confirmation"
class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" style="min-height: 48px;">
</div>
<div class="field-spacer">
<label for="role" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">ROLE</label>
<select name="role" id="role" required
class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" style="min-height: 48px;">
<option value="user" {{ old('role', $user->role) == 'user' ? 'selected' : '' }}>User</option>
<option value="admin" {{ old('role', $user->role) == 'admin' ? 'selected' : '' }}>Admin</option>
</select>
@error('role')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
</div>
</div>
<div class="flex justify-end">
<button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer">
Simpan Perubahan
</button>
</div>
</form>
</div>
@endsection

View File

@ -0,0 +1,84 @@
@extends('layouts.admin.app')
@section('title', '| Daftar Pengguna')
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Daftar Pengguna</h1>
<div>
<a href="{{ route('admin.users.create') }}" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors duration-200 inline-flex items-center">
Tambah
</a>
</div>
</div>
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 overflow-hidden rounded-lg">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr class="h-12">
<th scope="col" class="px-6 py-3 rounded-tl-lg">
NAMA
</th>
<th scope="col" class="px-6 py-3">
EMAIL
</th>
<th scope="col" class="px-6 py-3">
ROLE
</th>
<th scope="col" class="px-6 py-3 text-right rounded-tr-lg">
</th>
</tr>
</thead>
<tbody>
@forelse($users as $user)
<tr class="h-16 bg-white dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600 transition-colors hover:bg-gray-50 dark:hover:bg-gray-600">
<th scope="row" class="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap {{ $loop->last ? 'rounded-bl-lg' : '' }}">
{{ $user->name }}
</th>
<td class="px-6 py-4 text-gray-500 dark:text-gray-400">
{{ $user->email }}
</td>
<td class="px-6 py-4">
<span class="inline-flex text-xs leading-5 font-semibold rounded-full {{ $user->role === 'admin' ? 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200' : 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' }}">
{{ ucfirst($user->role) }}
</span>
</td>
<td class="px-6 py-4 text-right {{ $loop->last ? 'rounded-br-lg' : '' }}">
<div class="flex items-center justify-end space-x-4">
<a href="{{ route('admin.users.show', $user) }}" class="text-xs font-semibold uppercase hover:underline text-blue-600 dark:text-blue-400">
Detail
</a>
@if(Auth::id() !== $user->id)
<form action="{{ route('admin.users.destroy', $user) }}" method="POST" onsubmit="return confirm('Apakah Anda yakin ingin menghapus pengguna ini?');" class="inline">
@csrf
@method('DELETE')
<button type="submit" class="text-xs font-semibold uppercase hover:underline text-blue-600 dark:text-blue-400 cursor-pointer">
Hapus
</button>
</form>
@endif
<a href="{{ route('admin.users.edit', $user) }}" class="text-xs font-semibold uppercase hover:underline text-blue-600 dark:text-blue-400 cursor-pointer">
Edit
</a>
</div>
</td>
</tr>
@empty
<tr class="h-16 bg-white dark:bg-gray-700">
<td colspan="4" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400 rounded-b-lg">
Tidak ada pengguna yang ditemukan.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-6">
{{ $users->links() }}
</div>
</div>
@endsection

View File

@ -0,0 +1,106 @@
@extends('layouts.admin.app')
@section('title', '| Detail Pengguna')
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Detail Pengguna</h1>
<div>
<a href="{{ route('admin.users.index') }}" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 transition-colors duration-200 inline-flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
</svg>
Kembali
</a>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-gray-50 dark:bg-gray-700 p-6 rounded-lg border border-gray-200 dark:border-gray-600">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Informasi Pribadi</h2>
<div class="mb-4">
<p class="text-sm text-gray-500 dark:text-gray-400">ID</p>
<p class="text-lg text-gray-800 dark:text-white">{{ $user->id }}</p>
</div>
<div class="mb-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Nama</p>
<p class="text-lg text-gray-800 dark:text-white">{{ $user->name }}</p>
</div>
<div class="mb-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Email</p>
<p class="text-lg text-gray-800 dark:text-white">{{ $user->email }}</p>
</div>
<div class="mb-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Role</p>
<p class="text-lg text-gray-800 dark:text-white">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $user->role === 'admin' ? 'bg-purple-100 text-purple-800' : 'bg-blue-100 text-blue-800' }}">
{{ ucfirst($user->role) }}
</span>
</p>
</div>
<div class="mb-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Verifikasi Email</p>
<p class="text-lg text-gray-800 dark:text-white">
@if($user->email_verified_at)
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
Terverifikasi ({{ $user->email_verified_at->format('d M Y H:i') }})
</span>
@else
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
Belum Verifikasi
</span>
@endif
</p>
</div>
<div class="mb-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Login Google</p>
<p class="text-lg text-gray-800 dark:text-white">
@if($user->google_id)
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
Ya
</span>
@else
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">
Tidak
</span>
@endif
</p>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700 p-6 rounded-lg border border-gray-200 dark:border-gray-600">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Informasi Akun</h2>
<div class="mb-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Tanggal Dibuat</p>
<p class="text-lg text-gray-800 dark:text-white">{{ $user->created_at->format('d M Y H:i') }}</p>
</div>
<div class="mb-4">
<p class="text-sm text-gray-500 dark:text-gray-400">Terakhir Diupdate</p>
<p class="text-lg text-gray-800 dark:text-white">{{ $user->updated_at->format('d M Y H:i') }}</p>
</div>
@if(Auth::id() !== $user->id)
<div class="mt-8">
<h3 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">Tindakan</h3>
<form action="{{ route('admin.users.destroy', $user) }}" method="POST" onsubmit="return confirm('Apakah Anda yakin ingin menghapus pengguna ini? Tindakan ini tidak dapat dibatalkan.');">
@csrf
@method('DELETE')
<button type="submit" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-md transition-colors duration-200">
Hapus Pengguna
</button>
</form>
</div>
@endif
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,114 @@
@extends('layouts.admin.app')
@section('title', '| Tambah Bobot')
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Tambah Bobot Baru</h1>
<a href="{{ route('admin.weights.index') }}" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 cursor-pointer">
Kembali
</a>
</div>
@if ($errors->any())
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6 rounded" role="alert">
<p class="font-bold">Ada kesalahan dalam pengisian form:</p>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form action="{{ route('admin.weights.store') }}" method="POST" class="space-y-6">
@csrf
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="field-spacer">
<label for="category_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">KATEGORI</label>
<select name="category_id" id="category_id" style="min-height: 48px;" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" required>
<option value="">Pilih Kategori</option>
@foreach($categories as $category)
<option value="{{ $category->id }}" {{ old('category_id') == $category->id ? 'selected' : '' }}>{{ $category->name }}</option>
@endforeach
</select>
</div>
<div class="field-spacer">
<label for="subcategory_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">SUBKATEGORI (OPSIONAL)</label>
<select name="subcategory_id" id="subcategory_id" style="min-height: 48px;" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50">
<option value="">Pilih Subkategori</option>
</select>
</div>
<div class="field-spacer">
<label for="weight_value" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">BOBOT (1-100)</label>
<input type="number" name="weight_value" id="weight_value" min="1" max="100" value="{{ old('weight_value') }}" style="min-height: 48px; padding-top: 12px; padding-bottom: 12px; padding-left: 8px;" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" required>
</div>
</div>
<div class="flex justify-end">
<button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer">
Simpan
</button>
</div>
</form>
</div>
@push('scripts')
<script>
// Data kategori dari PHP
const categoriesData = @json($categoriesJson);
document.addEventListener('DOMContentLoaded', function() {
const categorySelect = document.getElementById('category_id');
const subcategorySelect = document.getElementById('subcategory_id');
// Debug: Tampilkan data kategori di console
console.log('Data Kategori dari PHP:', categoriesData);
// Fungsi untuk memperbarui dropdown subkategori
function updateSubcategories() {
const categoryId = parseInt(categorySelect.value);
console.log('Kategori yang dipilih ID:', categoryId);
// Reset dropdown subkategori
subcategorySelect.innerHTML = '<option value="">Pilih Subkategori</option>';
if (categoryId) {
// Ambil data subkategori dari kategori yang dipilih
const selectedCategory = categoriesData.find(c => c.id === categoryId);
console.log('Kategori yang dipilih:', selectedCategory);
if (selectedCategory && selectedCategory.subcategories && selectedCategory.subcategories.length > 0) {
console.log('Subkategori yang ditemukan:', selectedCategory.subcategories);
// Tambahkan opsi subkategori
selectedCategory.subcategories.forEach(subcategory => {
console.log('Menambahkan subkategori:', subcategory);
const option = document.createElement('option');
option.value = subcategory.id;
option.textContent = subcategory.name;
subcategorySelect.appendChild(option);
});
} else {
console.log('Tidak ada subkategori untuk kategori ini');
}
}
}
// Tambahkan event listener untuk perubahan kategori
categorySelect.addEventListener('change', updateSubcategories);
// Debug: Tampilkan event listener sudah terpasang
console.log('Event listener terpasang pada select kategori');
// Jalankan updateSubcategories jika ada kategori yang sudah dipilih
if (categorySelect.value) {
updateSubcategories();
}
});
</script>
@endpush
@endsection

View File

@ -0,0 +1,123 @@
@extends('layouts.admin.app')
@section('title', '| Edit Bobot')
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Edit Bobot</h1>
<a href="{{ route('admin.weights.index') }}" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 cursor-pointer">
Kembali
</a>
</div>
@if ($errors->any())
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6 rounded" role="alert">
<p class="font-bold">Ada kesalahan dalam pengisian form:</p>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form action="{{ route('admin.weights.update', $weight) }}" method="POST" class="space-y-6">
@csrf
@method('PUT')
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="field-spacer">
<label for="category_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">KATEGORI</label>
<select name="category_id" id="category_id" style="min-height: 48px;" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" required>
<option value="">Pilih Kategori</option>
@foreach($categories as $category)
<option value="{{ $category->id }}" {{ old('category_id', $weight->category_id) == $category->id ? 'selected' : '' }}>{{ $category->name }}</option>
@endforeach
</select>
</div>
<div class="field-spacer">
<label for="subcategory_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">SUBKATEGORI (OPSIONAL)</label>
<select name="subcategory_id" id="subcategory_id" style="min-height: 48px;" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50">
<option value="">Pilih Subkategori</option>
</select>
</div>
<div class="field-spacer">
<label for="weight_value" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">BOBOT (1-100)</label>
<input type="number" name="weight_value" id="weight_value" min="1" max="100" value="{{ old('weight_value', $weight->weight_value) }}" style="min-height: 48px; padding-top: 12px; padding-bottom: 12px; padding-left: 8px;" class="w-full py-3 pl-4 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" required>
</div>
</div>
<div class="flex justify-end">
<button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer">
Simpan Perubahan
</button>
</div>
</form>
</div>
@push('scripts')
<script>
// Data kategori dari PHP
const categoriesData = @json($categoriesJson);
const currentSubcategoryId = {{ $weight->subcategory_id ?? 'null' }};
document.addEventListener('DOMContentLoaded', function() {
const categorySelect = document.getElementById('category_id');
const subcategorySelect = document.getElementById('subcategory_id');
// Debug: Tampilkan data kategori di console
console.log('Data Kategori dari PHP:', categoriesData);
console.log('Current Subcategory ID:', currentSubcategoryId);
// Fungsi untuk memperbarui dropdown subkategori
function updateSubcategories() {
const categoryId = parseInt(categorySelect.value);
console.log('Kategori yang dipilih ID:', categoryId);
// Reset dropdown subkategori
subcategorySelect.innerHTML = '<option value="">Pilih Subkategori</option>';
if (categoryId) {
// Ambil data subkategori dari kategori yang dipilih
const selectedCategory = categoriesData.find(c => c.id === categoryId);
console.log('Kategori yang dipilih:', selectedCategory);
if (selectedCategory && selectedCategory.subcategories && selectedCategory.subcategories.length > 0) {
console.log('Subkategori yang ditemukan:', selectedCategory.subcategories);
// Tambahkan opsi subkategori
selectedCategory.subcategories.forEach(subcategory => {
console.log('Menambahkan subkategori:', subcategory);
const option = document.createElement('option');
option.value = subcategory.id;
option.textContent = subcategory.name;
// Pilih subkategori yang sedang diedit
if (subcategory.id === currentSubcategoryId) {
option.selected = true;
}
subcategorySelect.appendChild(option);
});
} else {
console.log('Tidak ada subkategori untuk kategori ini');
}
}
}
// Tambahkan event listener untuk perubahan kategori
categorySelect.addEventListener('change', updateSubcategories);
// Debug: Tampilkan event listener sudah terpasang
console.log('Event listener terpasang pada select kategori');
// Jalankan updateSubcategories jika ada kategori yang sudah dipilih
if (categorySelect.value) {
updateSubcategories();
}
});
</script>
@endpush
@endsection

View File

@ -0,0 +1,88 @@
@extends('layouts.admin.app')
@section('title', '| Pembobotan')
@section('content')
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 border border-gray-100 dark:border-gray-700">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Pembobotan Kriteria</h1>
<a href="{{ route('admin.weights.create') }}" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer">
Tambah Bobot
</a>
</div>
@if(session('success'))
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-6 rounded" role="alert">
<p>{{ session('success') }}</p>
</div>
@endif
@if(session('error'))
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6 rounded" role="alert">
<p>{{ session('error') }}</p>
</div>
@endif
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 overflow-hidden rounded-lg">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr class="h-12">
<th scope="col" class="px-6 py-3 rounded-tl-lg">
KATEGORI
</th>
<th scope="col" class="px-6 py-3">
SUBKATEGORI
</th>
<th scope="col" class="px-6 py-3">
BOBOT
</th>
<th scope="col" class="px-6 py-3">
BOBOT NORMALISASI
</th>
<th scope="col" class="px-6 py-3 text-right rounded-tr-lg">
AKSI
</th>
</tr>
</thead>
<tbody>
@forelse($weights as $weight)
<tr class="h-16 bg-gray-700 text-white border-b border-gray-600 transition-colors" style="cursor: pointer;" onmouseover="this.style.backgroundColor='#2B3544'" onmouseout="this.style.backgroundColor='#1F2937'">
<th scope="row" class="px-6 py-4 font-medium whitespace-nowrap {{ $loop->last ? 'rounded-bl-lg' : '' }}">
{{ $weight->category->name }}
</th>
<td class="px-6 py-4" style="color: rgba(255, 255, 255, 0.5);">
{{ $weight->subcategory ? $weight->subcategory->name : '-' }}
</td>
<td class="px-6 py-4" style="color: rgba(255, 255, 255, 0.5);">
{{ $weight->weight_value }}
</td>
<td class="px-6 py-4" style="color: rgba(255, 255, 255, 0.5);">
{{ number_format($weight->normalized_weight, 4) }}
</td>
<td class="px-6 py-4 text-right {{ $loop->last ? 'rounded-br-lg' : '' }}">
<div class="flex items-center justify-end space-x-4">
<a href="{{ route('admin.weights.edit', $weight) }}" class="text-xs font-semibold uppercase hover:underline" style="color: rgba(0, 109, 255);">
Edit
</a>
<form action="{{ route('admin.weights.destroy', $weight) }}" method="POST" onsubmit="return confirm('Apakah Anda yakin ingin menghapus bobot ini?');" class="inline">
@csrf
@method('DELETE')
<button type="submit" class="text-xs font-semibold uppercase hover:underline cursor-pointer" style="color: rgba(0, 109, 255);">
Hapus
</button>
</form>
</div>
</td>
</tr>
@empty
<tr class="h-16 bg-gray-700 text-white" style="cursor: pointer;" onmouseover="this.style.backgroundColor='#2B3544'" onmouseout="this.style.backgroundColor='#1F2937'">
<td colspan="5" class="px-6 py-4 text-center rounded-b-lg">
Tidak ada bobot yang ditemukan.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
@endsection

View File

@ -0,0 +1,114 @@
@extends('layouts.app')
@section('title', ' - Login')
@section('content')
<div class="min-h-screen flex items-center justify-center p-4 pt-20 md:pt-32">
<div class="max-w-md w-full">
<div style="border-radius: 32px;" class="bg-white dark:bg-gray-800 shadow-xl overflow-hidden p-6">
<!-- Logo
<div class="flex justify-center mb-4">
<a href="/" class="text-2xl font-bold text-gray-800 dark:text-white">Recaje</a>
</div> -->
<!-- Heading -->
<h1 class="text-2xl font-bold text-center mb-8 text-gray-800 dark:text-white">Masuk ke Akun Anda</h1>
<!-- Error Messages -->
@if ($errors->any())
<div class="mb-4 p-4 rounded-lg bg-red-50 dark:bg-red-900/50 border border-red-200 dark:border-red-800">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400 dark:text-red-300" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">
Terjadi kesalahan
</h3>
<div class="mt-2 text-sm text-red-700 dark:text-red-300">
<ul class="list-disc pl-5 space-y-1">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
</div>
</div>
</div>
@endif
<!-- Google Login Button -->
<div class="mb-4">
<a href="{{ route('login.google') }}" class="flex justify-center items-center px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 w-full">
<svg class="h-5 w-5 mr-2" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12.545,10.239v3.821h5.445c-0.712,2.315-2.647,3.972-5.445,3.972c-3.332,0-6.033-2.701-6.033-6.032s2.701-6.032,6.033-6.032c1.498,0,2.866,0.549,3.921,1.453l2.814-2.814C17.503,2.988,15.139,2,12.545,2C7.021,2,2.543,6.477,2.543,12s4.478,10,10.002,10c8.396,0,10.249-7.85,9.426-11.748L12.545,10.239z"/>
</svg>
Masuk dengan Google
</a>
</div>
<!-- Divider -->
<div class="relative mb-4">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300 dark:border-gray-600"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400">Atau masuk dengan</span>
</div>
</div>
<!-- Form -->
<form method="POST" action="{{ route('login') }}" class="space-y-6">
@csrf
<!-- Email -->
<div class="relative">
<input id="email" type="email" name="email" value="{{ old('email') }}" required autofocus
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800 dark:text-white"
placeholder="Email">
</div>
<!-- Password -->
<div class="relative">
<input id="password" type="password" name="password" required
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800 dark:text-white"
placeholder="Password">
</div>
<!-- Remember Me -->
<div class="flex items-center justify-between">
<!-- <div class="flex items-center">
<input id="remember" type="checkbox" name="remember" class="h-5 w-5 rounded-md border-gray-300 text-blue-500 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:ring-offset-0 cursor-pointer">
<label for="remember" class="ml-4 text-sm text-gray-600 dark:text-gray-400 cursor-pointer">Ingat saya</label>
</div> -->
@if (Route::has('password.request'))
<a href="{{ route('password.request') }}" class="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
Lupa password?
</a>
@endif
</div>
<!-- Submit Button -->
<div class="relative mb-3">
<button type="submit" class="w-full bg-gradient-to-r from-blue-500 to-purple-600 text-white py-3 rounded-lg hover:from-blue-600 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-300 font-medium cursor-pointer">
Masuk
</button>
</div>
</form>
<!-- Register Link -->
<div class="mt-8 text-center">
<p class="text-gray-600 dark:text-gray-400">
Belum punya akun?
<a href="{{ route('register') }}" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 font-medium">
Daftar sekarang
</a>
</p>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,65 @@
@extends('layouts.app')
@section('title', ' - Reset Password')
@section('content')
<div class="min-h-screen flex items-center justify-center p-4 pt-28 md:pt-32">
<div class="max-w-md w-full">
<div style="border-radius: 32px;" class="bg-white dark:bg-gray-800 shadow-xl overflow-hidden p-6">
<!-- Logo
<div class="flex justify-center mb-4">
<a href="/" class="text-2xl font-bold text-gray-800 dark:text-white">Recaje</a>
</div> -->
<!-- Heading -->
<h1 class="text-2xl font-bold text-center mb-8 text-gray-800 dark:text-white">Reset Password</h1>
@if (session('status'))
<div class="bg-green-100 dark:bg-green-800 text-green-800 dark:text-green-200 p-4 rounded-lg mb-6">
<div class="flex items-center">
<svg class="h-5 w-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
<span>{{ session('status') }}</span>
</div>
</div>
@endif
<!-- Form -->
<form method="POST" action="{{ route('password.email') }}" class="space-y-6">
@csrf
<p class="text-gray-600 dark:text-gray-400 mb-6 text-center">
Masukkan alamat email Anda dan kami akan mengirimkan link untuk reset password.
</p>
<!-- Email -->
<div class="relative">
<input id="email" type="email" name="email" value="{{ old('email') }}" required autofocus
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800 dark:text-white"
placeholder="Email">
@error('email')
<span class="text-red-500 text-sm mt-1">{{ $message }}</span>
@enderror
</div>
<!-- Submit Button -->
<div class="relative mb-3">
<button type="submit" class="w-full bg-gradient-to-r from-blue-500 to-purple-600 text-white py-3 rounded-lg hover:from-blue-600 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-300 font-medium cursor-pointer">
Kirim Link Reset Password
</button>
</div>
</form>
<!-- Login Link -->
<div class="mt-8 text-center">
<p class="text-gray-600 dark:text-gray-400">
<a href="{{ route('login') }}" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 font-medium">
Kembali ke halaman login
</a>
</p>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,68 @@
@extends('layouts.app')
@section('title', ' - Reset Password')
@section('content')
<div class="min-h-screen flex items-center justify-center p-4 pt-28 md:pt-32">
<div class="max-w-md w-full">
<div style="border-radius: 32px;" class="bg-white dark:bg-gray-800 shadow-xl overflow-hidden p-6">
<!-- Logo
<div class="flex justify-center mb-4">
<a href="/" class="text-2xl font-bold text-gray-800 dark:text-white">Recaje</a>
</div> -->
<!-- Heading -->
<h1 class="text-2xl font-bold text-center mb-8 text-gray-800 dark:text-white">Reset Password</h1>
<!-- Form -->
<form method="POST" action="{{ route('password.update') }}" class="space-y-6">
@csrf
<input type="hidden" name="token" value="{{ $token }}">
<!-- Email -->
<div class="relative">
<input id="email" type="email" name="email" value="{{ $email ?? old('email') }}" required readonly
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800 dark:text-white bg-gray-100 dark:bg-gray-600 cursor-not-allowed"
placeholder="Email">
@error('email')
<span class="text-red-500 text-sm mt-1">{{ $message }}</span>
@enderror
</div>
<!-- Password -->
<div class="relative">
<input id="password" type="password" name="password" required
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800 dark:text-white"
placeholder="Password Baru">
@error('password')
<span class="text-red-500 text-sm mt-1">{{ $message }}</span>
@enderror
</div>
<!-- Confirm Password -->
<div class="relative">
<input id="password_confirmation" type="password" name="password_confirmation" required
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800 dark:text-white"
placeholder="Konfirmasi Password Baru">
</div>
<!-- Submit Button -->
<div class="relative mb-3">
<button type="submit" class="w-full bg-gradient-to-r from-blue-500 to-purple-600 text-white py-3 rounded-lg hover:from-blue-600 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-300 font-medium cursor-pointer">
Reset Password
</button>
</div>
</form>
<!-- Login Link -->
<div class="mt-8 text-center">
<p class="text-gray-600 dark:text-gray-400">
<a href="{{ route('login') }}" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 font-medium">
Kembali ke halaman login
</a>
</p>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,120 @@
@extends('layouts.app')
@section('title', ' - Daftar')
@section('content')
<div class="min-h-screen flex items-center justify-center p-4 pt-28 md:pt-32">
<div class="max-w-md w-full">
<div style="border-radius: 32px;" class="bg-white dark:bg-gray-800 shadow-xl overflow-hidden p-6">
<!-- Logo
<div class="flex justify-center mb-4">
<a href="/" class="text-2xl font-bold text-gray-800 dark:text-white">Recaje</a>
</div> -->
<!-- Heading -->
<h1 class="text-2xl font-bold text-center mb-8 text-gray-800 dark:text-white">Buat Akun Baru</h1>
<!-- Google Login Button -->
<!-- <div class="mb-4">
<a href="{{ route('login.google') }}" class="flex justify-center items-center px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 w-full">
<svg class="h-5 w-5 mr-2" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12.545,10.239v3.821h5.445c-0.712,2.315-2.647,3.972-5.445,3.972c-3.332,0-6.033-2.701-6.033-6.032s2.701-6.032,6.033-6.032c1.498,0,2.866,0.549,3.921,1.453l2.814-2.814C17.503,2.988,15.139,2,12.545,2C7.021,2,2.543,6.477,2.543,12s4.478,10,10.002,10c8.396,0,10.249-7.85,9.426-11.748L12.545,10.239z"/>
</svg>
Daftar dengan Google
</a>
</div> -->
<!-- Divider -->
<!-- <div class="relative mb-4">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300 dark:border-gray-600"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400">Atau daftar dengan</span>
</div>
</div> -->
<!-- Form -->
<form method="POST" action="{{ route('register') }}" class="space-y-6">
@csrf
<!-- Name -->
<div class="relative">
<input id="name" type="text" name="name" value="{{ old('name') }}" required autofocus
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800 dark:text-white"
placeholder="Nama Lengkap">
@error('name')
<span class="text-red-500 text-sm mt-1">{{ $message }}</span>
@enderror
</div>
<!-- Email -->
<div class="relative">
<input id="email" type="email" name="email" value="{{ old('email') }}" required
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800 dark:text-white"
placeholder="Email">
@error('email')
<span class="text-red-500 text-sm mt-1">{{ $message }}</span>
@enderror
</div>
<!-- Password -->
<div class="relative">
<input id="password" type="password" name="password" required
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800 dark:text-white"
placeholder="Password">
@error('password')
<span class="text-red-500 text-sm mt-1">{{ $message }}</span>
@enderror
</div>
<!-- Confirm Password -->
<div class="relative">
<input id="password_confirmation" type="password" name="password_confirmation" required
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800 dark:text-white"
placeholder="Konfirmasi Password">
</div>
<!-- Terms and Privacy -->
<div class="flex items-start">
<div class="relative flex items-center">
<input id="terms" type="checkbox" name="terms" required
class="h-5 w-5 rounded-md border-gray-300 text-blue-500 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:ring-offset-0 cursor-pointer">
</div>
<div class="ml-3 text-sm">
<label for="terms" class="text-gray-600 dark:text-gray-400 cursor-pointer">
Saya menyetujui <a href="#" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">Syarat dan Ketentuan</a> serta <a href="#" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">Kebijakan Privasi</a>
</label>
</div>
</div>
<!-- Submit Button -->
<div class="relative mb-3">
<button type="submit" class="w-full bg-gradient-to-r from-blue-500 to-purple-600 text-white py-3 rounded-lg hover:from-blue-600 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-300 font-medium cursor-pointer">
Daftar
</button>
</div>
</form>
@if (session('status') == 'verification-link-sent')
<div class="bg-green-100 dark:bg-green-800 text-green-800 dark:text-green-200 p-4 rounded-lg mb-6">
Email verifikasi telah dikirim ke alamat email Anda.
</div>
<script>
window.location.href = "{{ route('verification.notice') }}";
</script>
@endif
<!-- Login Link -->
<div class="mt-8 text-center">
<p class="text-gray-600 dark:text-gray-400">
Sudah punya akun?
<a href="{{ route('login') }}" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 font-medium">
Masuk sekarang
</a>
</p>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Recaje - Verifikasi Email</title>
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />
<link href="{{ asset('css/disable-keyboard.css') }}" rel="stylesheet">
@vite(['resources/css/app.css', 'resources/js/app.js'])
<script src="{{ asset('js/disable-keyboard.js') }}"></script>
</head>
<body class="bg-gray-100 dark:bg-gray-900 flex flex-col min-h-screen">
<div class="flex-grow flex items-center justify-center p-4">
<div class="max-w-md w-full">
<div style="border-radius: 32px;" class="bg-white dark:bg-gray-800 shadow-xl overflow-hidden p-6">
<!-- Logo -->
<div class="flex justify-center mb-4">
<a href="/" class="text-2xl font-bold text-gray-800 dark:text-white">Recaje</a>
</div>
<!-- Heading -->
<h1 class="text-2xl font-bold text-center mb-4 text-gray-800 dark:text-white">Verifikasi Email Anda</h1>
@if (session('resent'))
<div class="bg-green-100 dark:bg-green-800 text-green-800 dark:text-green-200 p-4 rounded-lg mb-6">
Email verifikasi baru telah dikirim ke alamat email Anda.
</div>
@endif
<p class="text-gray-600 dark:text-gray-300 text-center mb-6 px-8">
Sebelum melanjutkan, silakan periksa email Anda untuk tautan verifikasi.
Jika Anda tidak menerima email tersebut, klik tombol di bawah untuk meminta email verifikasi baru.
</p>
<form method="POST" action="{{ route('verification.resend') }}" class="mb-6">
@csrf
<button type="submit" class="w-full bg-gradient-to-r from-blue-500 to-purple-600 text-white py-3 rounded-lg hover:from-blue-600 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-300 font-medium">
Kirim Ulang Email Verifikasi
</button>
</form>
<div class="text-center mt-8">
<a href="{{ route('logout') }}"
onclick="event.preventDefault(); document.getElementById('logout-form').submit();"
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 font-medium">
Keluar
</a>
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="hidden">
@csrf
</form>
</div>
</div>
</div>
</div>
<footer class="bg-gray-800 text-white py-8 mt-auto">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center">
<p>&copy; 2024 Recaje. All rights reserved.</p>
</div>
</div>
</footer>
</body>
</html>

View File

@ -0,0 +1,104 @@
@extends('layouts.app')
@section('title', ' - Semua Cafe')
@section('content')
<div class="bg-gray-200 dark:bg-gray-200 min-h-screen">
<div class="max-w-7xl mx-auto px-4 py-8">
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-8">
<h1 class="text-3xl font-bold text-gray-800 dark:text-gray-950 mb-4 md:mb-0">Daftar Cafe di Jember</h1>
<div class="w-full md:w-auto">
<form action="{{ route('all-cafes') }}" method="GET" class="flex space-x-2">
<div class="relative flex-grow">
<input
type="text"
name="search"
placeholder="Cari cafe..."
value="{{ request('search') }}"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
</div>
<button type="submit" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition-colors duration-200 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-800" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
</form>
</div>
</div>
<div class="mb-6">
<p class="text-gray-600 dark:text-gray-950">Temukan cafe favorit Anda dari berbagai pilihan yang tersedia.</p>
</div>
@if($cafes->isEmpty())
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-8 text-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-gray-400 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<h3 class="text-xl font-medium text-gray-700 dark:text-gray-300 mb-2">Belum Ada Data Cafe</h3>
<p class="text-gray-500 dark:text-gray-400">
Tidak ada data cafe yang tersedia saat ini.
</p>
</div>
@else
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
@foreach($cafes as $cafe)
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden transition duration-300 hover:shadow-2xl" style="transform: translateZ(0); backface-visibility: hidden;">
<div style="height: 200px; min-height: 200px; max-height: 200px; overflow: hidden; position: relative;" class="w-full flex-shrink-0 rounded-lg">
@if($cafe->gambar)
<img src="{{ asset($cafe->gambar) }}" alt="{{ $cafe->nama }}" style="width: 100%; height: 100%; object-fit: cover;">
@else
<div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;" class="bg-gradient-to-r from-gray-300 to-gray-200 dark:from-gray-700 dark:to-gray-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
@endif
</div>
<div class="p-4">
<h3 class="text-lg font-semibold text-gray-800 dark:text-white mb-2">{{ $cafe->nama }}</h3>
<!-- <p class="text-gray-600 dark:text-gray-300 mb-1 text-sm">{{ $cafe->lokasi }}</p> -->
@if($cafe->area)
<p class="text-gray-600 dark:text-gray-300 mb-3 text-sm">Area: {{ $cafe->area }}</p>
@endif
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div class="text-sm text-gray-600 dark:text-gray-400">
<div class="grid grid-cols-2 gap-2">
@foreach($cafe->ratings->take(4) as $rating)
<div class="col-span-1">
<span class="font-medium text-xs">{{ $rating->category->name }}</span>
<div class="text-yellow-500">
@for ($i = 0; $i < $rating->subcategory->value; $i++)
@endfor
@for ($i = $rating->subcategory->value; $i < 5; $i++)
@endfor
</div>
</div>
@endforeach
</div>
</div>
<div class="mt-4 text-right">
<a href="{{ route('cafes.show', $cafe->id) }}" class="text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition text-sm font-medium cursor-pointer">
Lihat Detail
</a>
</div>
</div>
</div>
</div>
@endforeach
</div>
<div class="mt-6 ">
{{ $cafes->links() }}
</div>
@endif
</div>
</div>
@endsection

View File

@ -0,0 +1,155 @@
@extends('layouts.app')
@section('title', ' - Rekomendasi Café')
@section('content')
<div class="py-8 px-4">
<div class="max-w-7xl mx-auto">
<h1 class="text-3xl font-bold mb-6 text-gray-800 dark:text-white text-center">Rekomendasikan Saya Café Terbaik</h1>
<p class="text-lg text-gray-600 dark:text-gray-300 text-center mb-8">Kami akan memberikan rekomendasi café terbaik berdasarkan preferensi Anda</p>
<!-- Main Container with Sidebar and Content -->
<div class="flex flex-col lg:flex-row gap-6">
<!-- Sidebar - Preference Form -->
<div class="w-full lg:w-1/3">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg overflow-hidden p-6 sticky top-6">
<h2 class="text-xl font-semibold mb-6 text-gray-800 dark:text-white">Preferensi Saya</h2>
<div class="space-y-6">
<!-- Atmosphere Preference -->
<div>
<p class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Suasana yang saya sukai:</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<label class="flex p-3 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700">
<input type="checkbox" class="h-5 w-5 rounded-md border-gray-300 text-blue-500 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:ring-offset-0 cursor-pointer mr-2">
<span class="text-gray-700 dark:text-gray-300">Tenang</span>
</label>
<label class="flex p-3 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700">
<input type="checkbox" class="h-5 w-5 rounded-md border-gray-300 text-blue-500 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:ring-offset-0 cursor-pointer mr-2">
<span class="text-gray-700 dark:text-gray-300">Nyaman</span>
</label>
<label class="flex p-3 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700">
<input type="checkbox" class="h-5 w-5 rounded-md border-gray-300 text-blue-500 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:ring-offset-0 cursor-pointer mr-2">
<span class="text-gray-700 dark:text-gray-300">Ramai</span>
</label>
<label class="flex p-3 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700">
<input type="checkbox" class="h-5 w-5 rounded-md border-gray-300 text-blue-500 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:ring-offset-0 cursor-pointer mr-2">
<span class="text-gray-700 dark:text-gray-300">Romantis</span>
</label>
</div>
</div>
<!-- Cafe Type Preference -->
<div>
<p class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Jenis café yang saya sukai:</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<label class="flex p-3 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700">
<input type="checkbox" class="h-5 w-5 rounded-md border-gray-300 text-blue-500 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:ring-offset-0 cursor-pointer mr-2">
<span class="text-gray-700 dark:text-gray-300">Coffee Shop</span>
</label>
<label class="flex p-3 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700">
<input type="checkbox" class="h-5 w-5 rounded-md border-gray-300 text-blue-500 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:ring-offset-0 cursor-pointer mr-2">
<span class="text-gray-700 dark:text-gray-300">Boba/Tea</span>
</label>
<label class="flex p-3 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700">
<input type="checkbox" class="h-5 w-5 rounded-md border-gray-300 text-blue-500 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:ring-offset-0 cursor-pointer mr-2">
<span class="text-gray-700 dark:text-gray-300">Dessert</span>
</label>
<label class="flex p-3 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700">
<input type="checkbox" class="h-5 w-5 rounded-md border-gray-300 text-blue-500 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:ring-offset-0 cursor-pointer mr-2">
<span class="text-gray-700 dark:text-gray-300">Breakfast</span>
</label>
</div>
</div>
<!-- Purpose Preference -->
<div>
<p class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Tujuan saya ke café:</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<label class="flex p-3 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700">
<input type="checkbox" class="h-5 w-5 rounded-md border-gray-300 text-blue-500 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:ring-offset-0 cursor-pointer mr-2">
<span class="text-gray-700 dark:text-gray-300">Kerja</span>
</label>
<label class="flex p-3 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700">
<input type="checkbox" class="h-5 w-5 rounded-md border-gray-300 text-blue-500 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:ring-offset-0 cursor-pointer mr-2">
<span class="text-gray-700 dark:text-gray-300">Santai</span>
</label>
<label class="flex p-3 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700">
<input type="checkbox" class="h-5 w-5 rounded-md border-gray-300 text-blue-500 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:ring-offset-0 cursor-pointer mr-2">
<span class="text-gray-700 dark:text-gray-300">Meeting</span>
</label>
<label class="flex p-3 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700">
<input type="checkbox" class="h-5 w-5 rounded-md border-gray-300 text-blue-500 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 focus:ring-offset-0 cursor-pointer mr-2">
<span class="text-gray-700 dark:text-gray-300">Kencan</span>
</label>
</div>
</div>
<div class="pt-4">
<button class="w-full bg-gradient-to-r from-blue-500 to-purple-600 text-white py-3 rounded-lg hover:from-blue-600 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-300 font-medium">
Berikan Rekomendasi
</button>
</div>
</div>
</div>
</div>
<!-- Main Content - Results -->
<div class="w-full lg:w-2/3">
<!-- Results Container -->
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg overflow-hidden p-6">
<h2 class="text-xl font-semibold mb-6 text-gray-800 dark:text-white">Rekomendasi Café untuk Anda</h2>
<!-- Results Placeholder -->
<div class="text-center py-16">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-gray-400 dark:text-gray-600 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg>
<h3 class="text-xl font-medium text-gray-500 dark:text-gray-400">Pilih preferensi Anda</h3>
<p class="mt-2 text-gray-500 dark:text-gray-500">Rekomendasi café akan muncul di sini</p>
</div>
<!-- Example Results (Hidden by default, would be shown after search) -->
<div class="hidden">
<div class="grid grid-cols-1 gap-6">
<!-- Cafe Item 1 -->
<div class="border border-gray-200 dark:border-gray-700 rounded-xl p-4 flex flex-col md:flex-row">
<div class="w-full md:w-1/3 mb-4 md:mb-0 md:mr-4">
<div class="h-48 bg-gray-300 dark:bg-gray-700 rounded-lg"></div>
</div>
<div class="w-full md:w-2/3">
<div class="flex justify-between items-start">
<h3 class="text-lg font-semibold text-gray-800 dark:text-white">Nama Café 1</h3>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
Rekomendasi 95%
</span>
</div>
<div class="flex items-center mb-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-yellow-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/>
</svg>
<span class="ml-1 text-gray-600 dark:text-gray-400">4.5 (120 reviews)</span>
</div>
<p class="text-gray-600 dark:text-gray-400 mb-2">Jakarta Selatan</p>
<p class="text-gray-600 dark:text-gray-400 mb-4">Café dengan suasana nyaman dan tenang, cocok untuk bekerja atau bersantai.</p>
<div class="flex flex-wrap gap-2 mb-4">
<span class="px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">Free Wifi</span>
<span class="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">Stop Kontak</span>
<span class="px-2 py-1 text-xs rounded-full bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">Nyaman</span>
</div>
<a href="#" class="inline-flex items-center text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
Lihat Detail
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,166 @@
@extends('layouts.app')
@section('title', ' - Detail Riwayat Pencarian')
@section('content')
<div class="bg-white dark:bg-gray-200 min-h-screen">
<div class="max-w-7xl mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold text-gray-800 dark:text-gray-950">Detail Riwayat Pencarian</h1>
<div class="flex space-x-4">
<a href="{{ route('cafe.search-history') }}" class="bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-600 px-4 py-2 rounded-lg transition-colors flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Kembali
</a>
</div>
</div>
<!-- Informasi pencarian -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Informasi Pencarian</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Tanggal Pencarian</dt>
<dd class="mt-1 text-base font-medium text-gray-800 dark:text-white">{{ $history->created_at->format('d M Y H:i') }}</dd>
</dl>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Lokasi</dt>
<dd class="mt-1 text-base font-medium text-gray-800 dark:text-white">
{{ $history->location ?: 'Semua Area' }}
</dd>
</div>
</div>
</div>
<!-- Bobot Kriteria -->
<!-- <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Bobot Kriteria</h2>
@if(!empty($weights))
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Nama Kriteria
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Bobot
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Bobot Normalisasi
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
@php
$totalWeight = array_sum($weights);
@endphp
@foreach($weights as $categoryId => $weight)
@php
$category = \App\Models\Category::find($categoryId);
$normWeight = $totalWeight > 0 ? ($weight / $totalWeight) * 100 : 0;
@endphp
@if($category)
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-gray-200">
{{ $category->name }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-gray-200">
{{ $weight }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-gray-200">
{{ number_format($normWeight, 2) }}%
</td>
</tr>
@endif
@endforeach
</tbody>
</table>
</div>
@else
<p class="text-gray-500 dark:text-gray-400">Tidak ada data bobot kriteria</p>
@endif
</div> -->
<!-- Hasil -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Hasil Pencarian</h2>
@if($rankedCafes->isEmpty())
<p class="text-gray-500 dark:text-gray-400">Tidak ada hasil pencarian</p>
@else
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($rankedCafes as $cafe)
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300 border border-gray-100 dark:border-gray-700">
@if($cafe->gambar)
<img src="{{ asset($cafe->gambar) }}" alt="{{ $cafe->nama }}" class="w-full h-48 object-cover">
@else
<div class="w-full h-48 bg-gradient-to-r from-gray-300 to-gray-200 dark:from-gray-700 dark:to-gray-600 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
@endif
<div class="p-4">
<h3 class="text-xl font-semibold text-gray-800 dark:text-white mb-2">{{ $cafe->nama }}</h3>
<!-- <p class="text-gray-600 dark:text-gray-300 mb-3">{{ $cafe->lokasi }}</p> -->
<!-- Detail skor -->
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div class="flex justify-between items-center mb-2">
<span class="font-medium text-gray-700 dark:text-gray-300">Skor SMART</span>
<span class="text-lg font-bold text-blue-600 dark:text-blue-400">{{ number_format($cafe->smart_score, 2) }}</span>
</div>
@if(!empty($cafe->smart_details))
<details class="group mt-3">
<summary class="font-medium text-sm cursor-pointer text-gray-600 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400 flex items-center">
<span>Detail Perhitungan</span>
<svg class="w-4 h-4 ml-1.5 transform transition-transform duration-200 group-open:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</summary>
<div class="mt-2 text-xs bg-gray-50 dark:bg-gray-800 p-2 rounded-lg">
<table class="min-w-full">
<thead>
<tr>
<th class="text-left py-1 px-2 text-gray-500">Kriteria</th>
<th class="text-right py-1 px-2 text-gray-500">Bobot</th>
<th class="text-right py-1 px-2 text-gray-500">Nilai</th>
<th class="text-right py-1 px-2 text-gray-500">Skor</th>
</tr>
</thead>
<tbody>
@foreach($cafe->smart_details as $category => $info)
<tr>
<td class="text-left py-1 px-2 text-gray-700 dark:text-gray-300">{{ $category }}</td>
<td class="text-right py-1 px-2 text-gray-700 dark:text-gray-300">{{ number_format($info['bobot_normal'] * 100, 0) }}%</td>
<td class="text-right py-1 px-2 text-gray-700 dark:text-gray-300">{{ number_format($info['nilai'], 1) }}</td>
<td class="text-right py-1 px-2 text-gray-700 dark:text-gray-300">{{ number_format($info['skor_kriteria'], 2) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</details>
@endif
<div class="mt-4 text-right">
<a href="{{ route('cafes.show', $cafe->id) }}" class="text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition text-sm font-medium">
Lihat Detail Café
</a>
</div>
</div>
</div>
</div>
@endforeach
</div>
@endif
</div>
</div>
</div>
@endsection

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,199 @@
@extends('layouts.app')
@section('title', ' - Riwayat Pencarian')
@section('content')
<div class="bg-gray-200 dark:bg-gray-200 min-h-screen">
<div class="max-w-7xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-2 text-gray-800 dark:text-gray-950">Riwayat Pencarian</h1>
<div class="mb-6 flex flex-col sm:flex-row justify-between items-start sm:items-left gap-4">
<p class="text-gray-600 dark:text-gray-950">Berikut adalah daftar pencarian yang pernah Anda lakukan.</p>
<!-- <a href="{{ route('search-cafe') }}" class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition-colors flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a7 7 0 100 14 7 7 0 000-14zm-9 7a9 9 0 1118 0 9 9 0 01-18 0z" clip-rule="evenodd" />
<path fill-rule="evenodd" d="M13.586 11.586a1 1 0 01-1.414 1.414L10 10.828 7.879 13a1 1 0 01-1.414-1.414l2.828-2.829a1 1 0 011.414 0l2.879 2.829z" clip-rule="evenodd" />
</svg>
Pencarian Baru
</a> -->
</div>
@if($histories->isEmpty())
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-8 text-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-gray-400 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h3 class="text-xl font-medium text-gray-700 dark:text-gray-950 mb-2">Belum Ada Riwayat Pencarian</h3>
<p class="text-gray-500 dark:text-gray-950">
Anda belum pernah melakukan pencarian cafe. Mulai pencarian pertama Anda sekarang!
</p>
</div>
@else
<div class="bg-gray-200 dark:bg-gray-900 rounded-lg shadow-md overflow-hidden">
<style>
@media (min-width: 768px) {
.desktop-table { display: table; }
.mobile-cards { display: none; }
}
@media (max-width: 767px) {
.desktop-table { display: none; }
.mobile-cards { display: block; }
}
</style>
<!-- Desktop View -->
<div class="w-full overflow-x-auto rounded-lg">
<table class="desktop-table w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-100">
<tr>
<th scope="col" class="px-3 lg:px-6 py-3 text-center text-xs font-medium text-gray-200 dark:text-gray-200 uppercase tracking-wider">
Tanggal Pencarian
</th>
<th scope="col" class="px-3 lg:px-6 py-3 text-center text-xs font-medium text-gray-200 dark:text-gray-200 uppercase tracking-wider">
Kriteria
</th>
<th scope="col" class="px-3 lg:px-6 py-3 text-center text-xs font-medium text-gray-200 dark:text-gray-200 uppercase tracking-wider">
Hasil Teratas
</th>
<th scope="col" class="px-3 lg:px-6 py-3 text-center text-xs font-medium text-gray-200 dark:text-gray-200 uppercase tracking-wider">
Aksi
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
@foreach($histories as $history)
<tr class="hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors my-2"> <td class="px-3 lg:px-6 py-5 lg:py-8 whitespace-nowrap"> <div class="text-xs text-gray-900 dark:text-gray-100">{{ $history->created_at->format('d M Y') }}</div> <div class="text-xs text-gray-500 dark:text-gray-400">{{ $history->created_at->format('H:i') }}</div> </td> <td class="px-3 lg:px-6 py-5 lg:py-8">
<div class="text-sm text-gray-900 dark:text-gray-100">
@php
$criteria = [];
if (!empty($history->weights)) {
foreach ($history->weights as $categoryId => $weight) {
if ($weight > 0) {
$category = \App\Models\Category::find($categoryId);
if ($category) {
$criteria[] = $category->name . ' (' . round(($weight / array_sum($history->weights) * 100), 0) . '%)';
}
}
}
}
@endphp
@if(count($criteria) > 0)
<div class="flex flex-wrap gap-1">
@foreach($criteria as $c)
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
{{ $c }}
</span>
@endforeach
</div>
@else
<span class="text-gray-500 dark:text-gray-400 text-xs">Tidak ada kriteria khusus</span>
@endif
</div>
</td>
<td class="px-3 lg:px-6 py-4 lg:py-6">
@if(!empty($history->results))
<div class="space-y-2">
@php $count = 0; @endphp
@foreach($history->results as $cafeId => $result)
@if($count < 3)
<div class="flex items-center gap-2">
<span class="flex items-center justify-center w-5 h-5 rounded-full bg-gray-200 dark:bg-gray-700 text-xs font-medium text-gray-800 dark:text-gray-200">{{ $count + 1 }}</span>
<span class="text-sm text-gray-900 dark:text-gray-100">
{{ $result['name'] ?? 'Cafe #'.$cafeId }}
<span class="text-blue-500 dark:text-blue-400 ml-1 text-xs font-medium">({{ number_format($result['score'], 2) }})</span>
</span>
</div>
@php $count++; @endphp
@endif
@endforeach
</div>
@else
<span class="text-gray-500 dark:text-gray-400 text-sm">Tidak ada hasil</span>
@endif
</td>
<td class="px-3 lg:px-6 py-5 lg:py-8 whitespace-nowrap text-right text-sm font-medium"> <a href="{{ route('cafe.smart-search-detail', $history->id) }}" class="inline-flex items-center px-2 py-1 lg:px-3 lg:py-1.5 border border-transparent text-xs lg:text-sm leading-4 rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-800"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> </svg> Detail </a> </td>
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- Mobile View (smaller than md) -->
<div class="mobile-cards space-y-4">
@foreach($histories as $history)
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow mb-4">
<div class="flex justify-between items-start mb-3">
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $history->created_at->format('d M Y') }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">{{ $history->created_at->format('H:i') }}</div>
</div>
<a href="{{ route('cafe.smart-search-detail', $history->id) }}" class="inline-flex items-center px-2 py-1 border border-transparent text-xs rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-800">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
Detail
</a>
</div>
<!-- <div class="mb-3">
<div class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400 mb-1">Kriteria:</div>
@php
$criteria = [];
if (!empty($history->weights)) {
foreach ($history->weights as $categoryId => $weight) {
if ($weight > 0) {
$category = \App\Models\Category::find($categoryId);
if ($category) {
$criteria[] = $category->name . ' (' . round(($weight / array_sum($history->weights) * 100), 0) . '%)';
}
}
}
}
@endphp
@if(count($criteria) > 0)
<div class="flex flex-wrap gap-1">
@foreach($criteria as $c)
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
{{ $c }}
</span>
@endforeach
</div>
@else
<span class="text-gray-500 dark:text-gray-400 text-xs">Tidak ada kriteria khusus</span>
@endif
</div> -->
<div>
<div class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400 mb-2">Hasil Teratas:</div>
@if(!empty($history->results))
<div class="space-y-2">
@php $count = 0; @endphp
@foreach($history->results as $cafeId => $result)
@if($count < 3)
<div class="flex items-center gap-2">
<span class="flex items-center justify-center w-5 h-5 rounded-full bg-gray-200 dark:bg-gray-700 text-xs font-medium text-gray-800 dark:text-gray-200">{{ $count + 1 }}</span>
<span class="text-sm text-gray-900 dark:text-gray-100">
{{ $result['name'] ?? 'Cafe #'.$cafeId }}
<span class="text-blue-500 dark:text-blue-400 ml-1 text-xs font-medium">({{ number_format($result['score'], 2) }})</span>
</span>
</div>
@php $count++; @endphp
@endif
@endforeach
</div>
@else
<span class="text-gray-500 dark:text-gray-400 text-sm">Tidak ada hasil</span>
@endif
</div>
</div>
@endforeach
</div>
</div>
<div class="mt-6">
{{ $histories->links() }}
</div>
@endif
</div>
</div>
@endsection

View File

@ -0,0 +1,679 @@
@extends('layouts.app')
@section('title', ' - Pencarian Café')
@php
// Tentukan langkah saat ini berdasarkan parameter URL
if (request()->has('weight')) {
$currentStep = 3; // Jika ada hasil pencarian (parameter weight), berarti di langkah hasil
} elseif (old('criteria') || request()->has('criteria')) {
$currentStep = 2; // Jika ada kriteria yang dipilih, berarti di langkah kriteria
} else {
$currentStep = 1; // Default langkah awal
}
@endphp
@section('stepper')
<x-stepper :currentStep="$currentStep" />
@endsection
@section('content')
<script>
//<![CDATA[
// Cek apakah halaman memiliki hasil pencarian (parameter weight di URL)
const hasSearchResults = {{ request()->has('weight') ? 'true' : 'false' }};
// Global data untuk menyimpan nilai penting setiap kriteria
const importanceValues = {};
//]]>
</script>
<script>
// Label untuk tingkat kepentingan
const importanceLabels = {
1: "Sangat tidak penting (1)",
2: "Tidak penting (2)",
3: "Kurang penting (3)",
4: "Agak penting (4)",
5: "Sedang (5)",
6: "Cukup penting (6)",
7: "Penting (7)",
8: "Sangat penting (8)",
9: "Sangat penting sekali (9)",
10: "Paling penting (10)"
};
// Fungsi untuk update nilai slider yang dipanggil langsung dari oninput HTML
function updateSliderValue(id, value) {
// Update nilai penting di object global
importanceValues[id] = parseInt(value);
// Update tampilan label tingkat kepentingan
const importanceDisplay = document.getElementById(`importance_value_${id}`);
if (importanceDisplay) {
importanceDisplay.textContent = importanceLabels[value];
}
// Hitung ulang bobot dan perbarui tampilan
calculateAndUpdateWeights();
}
// Fungsi untuk menghitung bobot berdasarkan tingkat kepentingan
function calculateAndUpdateWeights() {
let totalImportance = 0;
Object.values(importanceValues).forEach(value => {
totalImportance += value;
});
// Hitung bobot berdasarkan proporsi kepentingan
const weights = {};
let totalWeight = 0;
if (totalImportance > 0) {
// Hitung berdasarkan proporsi
Object.keys(importanceValues).forEach(id => {
weights[id] = Math.floor((importanceValues[id] / totalImportance) * 100);
totalWeight += weights[id];
});
// Distribusikan sisa persen
if (totalWeight < 100) {
// Urutkan ID berdasarkan tingkat kepentingan (tertinggi dulu)
const sortedIds = Object.keys(importanceValues).sort((a, b) =>
importanceValues[b] - importanceValues[a]
);
// Distribusikan sisa ke nilai tertinggi
let remainder = 100 - totalWeight;
for (let i = 0; i < remainder; i++) {
weights[sortedIds[i % sortedIds.length]]++;
}
}
} else {
// Distribusi merata jika semua nilai penting adalah 0
const equalWeight = Math.floor(100 / Object.keys(importanceValues).length);
let remainder = 100 - (equalWeight * Object.keys(importanceValues).length);
Object.keys(importanceValues).forEach((id, index) => {
weights[id] = equalWeight + (index < remainder ? 1 : 0);
});
}
// Update tampilan bobot dan input tersembunyi
Object.keys(weights).forEach(id => {
const weightDisplay = document.getElementById(`weight_display_${id}`);
const hiddenInput = document.getElementById(`hidden_weight_${id}`);
if (weightDisplay) {
weightDisplay.textContent = `${weights[id]}%`;
// Terapkan kode warna berdasarkan nilai bobot
if (weights[id] >= 30) {
weightDisplay.className = 'text-sm font-medium text-green-600 dark:text-green-400';
} else if (weights[id] >= 15) {
weightDisplay.className = 'text-sm font-medium text-blue-600 dark:text-blue-400';
} else {
weightDisplay.className = 'text-sm font-medium text-gray-700 dark:text-gray-300';
}
}
if (hiddenInput) {
hiddenInput.value = weights[id];
}
});
// Update indikator total bobot
const totalWeightIndicator = document.getElementById('total_weight_indicator');
if (totalWeightIndicator) {
const calculatedTotal = Object.values(weights).reduce((sum, w) => sum + w, 0);
totalWeightIndicator.textContent = `Total: ${calculatedTotal}%`;
if (calculatedTotal === 100) {
totalWeightIndicator.classList.remove('bg-red-100', 'text-red-800', 'dark:bg-red-900', 'dark:text-red-300');
totalWeightIndicator.classList.add('bg-green-100', 'text-green-800', 'dark:bg-green-900', 'dark:text-green-300');
} else {
totalWeightIndicator.classList.remove('bg-green-100', 'text-green-800', 'dark:bg-green-900', 'dark:text-green-300');
totalWeightIndicator.classList.add('bg-red-100', 'text-red-800', 'dark:bg-red-900', 'dark:text-red-300');
}
}
return Object.values(weights).reduce((sum, w) => sum + w, 0) === 100;
}
// Fungsi untuk cek apakah halaman ini hasil dari submit form
function checkIfSearchResults() {
return hasSearchResults;
}
// Update fungsi showSection untuk memanggil updateStepProgress
function showSection1() {
document.getElementById('section1').style.display = 'block';
document.getElementById('section2').style.display = 'none';
document.getElementById('section3').style.display = 'none';
updateStepperVisually(1);
window.scrollTo(0, 0);
}
function showSection2() {
document.getElementById('section1').style.display = 'none';
document.getElementById('section2').style.display = 'block';
document.getElementById('section3').style.display = 'none';
updateStepperVisually(2);
window.scrollTo(0, 0);
}
function showSection3() {
document.getElementById('section1').style.display = 'none';
document.getElementById('section2').style.display = 'none';
document.getElementById('section3').style.display = 'block';
updateStepperVisually(3);
window.scrollTo(0, 0);
}
// Fungsi untuk memperbarui tampilan stepper
function updateStepperVisually(step) {
// Ambil semua step-item dari stepper
const stepItems = document.querySelectorAll('.step-item');
stepItems.forEach((item, index) => {
const stepNum = index + 1;
const circleDiv = item.querySelector('.w-10');
const textSpan = item.querySelector('.text-sm');
if (stepNum <= step) {
// Langkah aktif atau sudah dilalui
circleDiv.classList.remove('bg-gray-200', 'dark:bg-gray-700', 'text-gray-600', 'dark:text-gray-400');
circleDiv.classList.add('bg-blue-600', 'text-white');
textSpan.classList.remove('text-gray-500', 'dark:text-gray-400');
textSpan.classList.add('text-blue-600', 'dark:text-blue-400');
} else {
// Langkah yang belum aktif
circleDiv.classList.remove('bg-blue-600', 'text-white');
circleDiv.classList.add('bg-gray-200', 'dark:bg-gray-700', 'text-gray-600', 'dark:text-gray-400');
textSpan.classList.remove('text-blue-600', 'dark:text-blue-400');
textSpan.classList.add('text-gray-500', 'dark:text-gray-400');
}
// Update status aktif di class
if (stepNum <= step) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
}
</script>
<script>
//<![CDATA[
// Initialize stepper
document.addEventListener('DOMContentLoaded', function() {
try {
console.log('DOM Content Loaded.');
// Inisialisasi nilai penting dan bobot dari URL jika ada
const sliders = document.querySelectorAll('.importance-slider');
sliders.forEach(slider => {
const id = slider.dataset.id;
const requestWeight = slider.dataset.requestWeight;
// Jika ada bobot dari request
if (requestWeight) {
// Cari nilai importance yang sesuai dengan bobot
for (let i = 1; i <= 10; i++) {
importanceValues[id] = i;
calculateAndUpdateWeights();
const weight = document.getElementById(`hidden_weight_${id}`).value;
if (parseInt(weight) === parseInt(requestWeight)) {
slider.value = i;
updateSliderValue(id, i);
break;
}
}
} else {
// Default nilai awal
const defaultValue = slider.value;
importanceValues[id] = parseInt(defaultValue);
}
});
// Update semua bobot
calculateAndUpdateWeights();
// Inisialisasi stepper berdasarkan variabel PHP
const currentStep = {{ $currentStep }};
// Tampilkan section sesuai langkah
if (checkIfSearchResults()) {
showSection3();
} else if (currentStep === 2) {
showSection2();
} else {
showSection1();
}
// Form submission validation - ensure weights sum to 100%
const smartForm = document.getElementById('smartForm');
if (smartForm) {
smartForm.addEventListener('submit', function(event) {
if (!calculateAndUpdateWeights()) {
event.preventDefault();
alert('Total bobot harus 100%. Silakan sesuaikan tingkat kepentingan.');
showSection1();
}
});
}
} catch (error) {
console.error('Terjadi error saat inisialisasi halaman:', error);
}
});
//]]>
</script>
<div class="bg-white dark:bg-gray-900 min-h-screen">
<div class="max-w-7xl mx-auto px-4 py-8">
<h1 class="text-4xl font-bold mb-4 text-gray-800 dark:text-white text-center">Pencarian Café</h1>
<p class="text-lg text-gray-600 dark:text-gray-300 text-center mb-10">Temukan café yang sesuai dengan preferensi dan kebutuhan Anda</p>
<!-- Search Form -->
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg overflow-hidden p-6 mb-10">
<form id="smartForm" action="{{ route('search-cafe') }}" method="GET">
<!-- SECTION 1: Preference Weights -->
<div id="section1">
<h2 class="text-2xl font-semibold mb-6 text-gray-800 dark:text-white border-b pb-3">Langkah 1: Tentukan Prioritas Anda</h2>
<p class="text-gray-600 dark:text-gray-300 mb-6">Geser slider untuk menunjukkan seberapa penting setiap kriteria. Bobot akan dihitung otomatis (total 100%).</p>
@php
$categories = \App\Models\Category::all();
$categoryCount = $categories->count();
$defaultImportance = 5; // Default tingkat kepentingan
$requestWeights = request('weight') ?? []; // Ambil bobot dari request jika ada
@endphp
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach($categories as $index => $category)
<div class="bg-white dark:bg-gray-800 p-4 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 hover:shadow-md transition-shadow duration-300">
<label for="importance_{{ $category->id }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ $category->name }}</label>
<div class="flex flex-col">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-gray-500">Kurang</span>
<span class="text-xs text-gray-500">Penting</span>
</div>
<div class="flex items-center justify-between mb-1">
<span class="text-xs text-gray-500">1</span>
<span class="text-xs text-gray-500">10</span>
</div>
<input
type="range"
min="1"
max="10"
value="{{ $defaultImportance }}"
id="importance_{{ $category->id }}"
class="importance-slider w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-blue-600"
data-id="{{ $category->id }}"
data-request-weight="{{ $requestWeights[$category->id] ?? '' }}"
oninput="updateSliderValue('{{ $category->id }}', this.value)"
>
<div class="flex justify-between mt-2">
<span id="importance_value_{{ $category->id }}" class="text-sm font-medium text-blue-600 dark:text-blue-400">Sedang (5)</span>
<span id="weight_display_{{ $category->id }}" class="text-sm font-medium text-gray-700 dark:text-gray-300">0%</span>
</div>
<input type="hidden" name="weight[{{ $category->id }}]" id="hidden_weight_{{ $category->id }}" value="0">
</div>
</div>
@endforeach
</div>
<div class="mt-8 flex justify-end">
<button type="button" id="nextButton1" onclick="showSection2()" class="bg-blue-500 text-white py-2 px-6 rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-300 font-medium flex items-center">
Lanjut ke Kriteria <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-1" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L12.586 11H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
</button>
</div>
</div>
<!-- SECTION 2: Criteria Selection & Location -->
<div id="section2" style="display: none;">
<h2 class="text-2xl font-semibold mb-6 text-gray-800 dark:text-white border-b pb-3">Langkah 2: Pilih Kriteria Spesifik (Opsional)</h2>
<p class="text-gray-600 dark:text-gray-300 mb-6">Pilih kriteria spesifik jika ada preferensi khusus. Café yang cocok akan mendapat nilai lebih.</p>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@php
$categoriesWithSubs = \App\Models\Category::all();
$requestCriteria = request('criteria') ?? [];
$requestLokasi = request('lokasi');
@endphp
@foreach($categoriesWithSubs as $category)
<div class="bg-white dark:bg-gray-800 p-4 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 hover:shadow-md transition-shadow duration-300">
<label for="criteria_{{ $category->id }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ $category->name }}</label>
<select id="criteria_{{ $category->id }}" name="criteria[{{ $category->id }}]" class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-800 dark:text-white">
<option value="">Semua {{ strtolower($category->name) }}</option>
@php
$subcategories = \App\Models\Subcategory::where('category_id', $category->id)->get();
@endphp
@foreach($subcategories as $subcategory)
<option value="{{ $subcategory->id }}" {{ isset($requestCriteria[$category->id]) && $requestCriteria[$category->id] == $subcategory->id ? 'selected' : '' }}>
{{ $subcategory->name }}
</option>
@endforeach
</select>
</div>
@endforeach
</div>
<div class="mt-8 flex justify-between items-center">
<button type="button" id="backButton1" onclick="showSection1()" class="bg-gray-200 text-gray-700 py-2 px-6 rounded-lg hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-all duration-300 font-medium flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L7.414 9H15a1 1 0 110 2H7.414l2.293 2.293a1 1 0 010 1.414z" clip-rule="evenodd"/></svg>
Kembali ke Bobot
</button>
<button type="submit" id="submitButton" class="bg-gradient-to-r from-blue-500 to-purple-600 text-white py-3 px-8 rounded-lg hover:from-blue-600 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-300 font-medium shadow-md">
<span class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
</svg>
Cari Café Rekomendasi
</span>
</button>
</div>
</div>
</form>
</div>
<!-- SECTION 3: Results Container -->
<div id="section3" style="display: none;">
@if(request()->has('weight'))
<div id="results" class="mb-10">
<div class="flex justify-between items-center mb-6 border-b pb-3">
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white">Hasil Rekomendasi SMART</h2>
<button type="button" id="backToSearchButton" onclick="showSection2()" class="bg-gray-200 text-gray-700 py-2 px-4 rounded-lg hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-all duration-300 font-medium text-sm flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L7.414 9H15a1 1 0 110 2H7.414l2.293 2.293a1 1 0 010 1.414z" clip-rule="evenodd"/></svg>
Kembali ke Pencarian
</button>
</div>
@php
// Implementasi algoritma SMART
$cafes = \App\Models\Cafe::with(['ratings.subcategory', 'ratings.category'])->get();
$weights = request('weight') ?? [];
$criterias = request('criteria') ?? [];
$lokasi = request('lokasi');
$selectedCriteria = [];
if (!empty($criterias)) {
foreach ($criterias as $categoryId => $subcategoryId) {
if ($subcategoryId) {
$category = \App\Models\Category::find($categoryId);
$subcategory = \App\Models\Subcategory::find($subcategoryId);
if ($category && $subcategory) {
$selectedCriteria[] = $category->name . ': ' . $subcategory->name;
}
}
}
}
// Filter berdasarkan lokasi jika ada
if ($lokasi) {
$cafes = $cafes->filter(function($cafe) use ($lokasi) {
return stripos($cafe->lokasi, $lokasi) !== false;
});
}
// Hitung total semua bobot untuk normalisasi (atau default 100 jika kosong)
$totalWeight = array_sum($weights);
if ($totalWeight == 0) {
// Jika tidak ada bobot, buat bobot merata
$categories = \App\Models\Category::all();
foreach ($categories as $category) {
$weights[$category->id] = 100 / $categories->count();
}
$totalWeight = 100;
}
// Hitung skor SMART untuk setiap cafe
$cafeScores = [];
$debugOutput = "";
// Metode alternatif yang lebih langsung - ambil data dari database
foreach ($cafes as $cafe) {
$score = 0;
$debugInfo = [];
// Query langsung ke database untuk mendapatkan data yang diperlukan
$ratings = \Illuminate\Support\Facades\DB::table('cafe_ratings')
->join('subcategories', 'cafe_ratings.subcategory_id', '=', 'subcategories.id')
->join('categories', 'cafe_ratings.category_id', '=', 'categories.id')
->where('cafe_ratings.cafe_id', $cafe->id)
->select(
'cafe_ratings.category_id',
'categories.name as category_name',
'cafe_ratings.subcategory_id',
'subcategories.name as subcategory_name',
'subcategories.value'
)
->get();
$debugOutput .= "<div style='margin-bottom:5px;padding:5px;background:#f8f8f8;border:1px solid #ddd;'>";
$debugOutput .= "<strong>Cafe: " . $cafe->nama . "</strong><br>";
foreach ($ratings as $rating) {
// Jika ada bobot untuk kategori ini
if (isset($weights[$rating->category_id])) {
$weight = floatval($weights[$rating->category_id]);
$normalizedWeight = $totalWeight > 0 ? $weight / $totalWeight : 0;
// Pastikan nilai valid
$value = max(1, intval($rating->value));
// Bonus nilai jika kriteria dipilih
if (isset($criterias[$rating->category_id]) &&
$criterias[$rating->category_id] == $rating->subcategory_id) {
$value = 5; // Nilai maksimal
}
// Hitung skor
$ratingScore = $normalizedWeight * $value;
$score += $ratingScore;
$debugOutput .= "&nbsp; " . $rating->category_name . ": " .
"Weight=" . number_format($normalizedWeight * 100, 1) . "%, " .
"Value=" . $value . ", " .
"Score=" . number_format($ratingScore, 2) . "<br>";
// Simpan untuk debug
$debugInfo[$rating->category_name] = [
'bobot' => $weight,
'bobot_normal' => $normalizedWeight,
'nilai' => $value,
'nilai_asli' => $rating->value,
'subcategory' => $rating->subcategory_name,
'skor_kriteria' => $ratingScore
];
}
}
$debugOutput .= "Total Score: " . number_format($score, 2) . "</div>";
$cafeScores[$cafe->id] = [
'score' => $score,
'debug' => $debugInfo
];
}
// Urutkan cafe berdasarkan skor tertinggi
uasort($cafeScores, function($a, $b) {
return $b['score'] <=> $a['score'];
});
// Ambil id cafe yang sudah diurutkan
$rankedCafeIds = array_keys($cafeScores);
// Urutkan koleksi cafe sesuai skor
$rankedCafes = $cafes->sortBy(function($cafe) use ($rankedCafeIds) {
return array_search($cafe->id, $rankedCafeIds);
});
@endphp
@if(count($selectedCriteria) > 0 || $lokasi)
<div class="mb-6 bg-gray-50 dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium text-gray-700 dark:text-gray-300 mb-2">Kriteria Pencarian:</h3>
<div class="flex flex-wrap gap-2">
@foreach($selectedCriteria as $criteria)
<span class="px-3 py-1 text-sm font-medium rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">{{ $criteria }}</span>
@endforeach
@if($lokasi)
<span class="px-3 py-1 text-sm font-medium rounded-full bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">Lokasi: {{ $lokasi }}</span>
@endif
</div>
</div>
@endif
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@if($rankedCafes->isEmpty())
<div class="col-span-full text-center py-12 bg-white dark:bg-gray-800 rounded-lg shadow-md">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-gray-400 dark:text-gray-600 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<p class="text-lg font-medium text-gray-600 dark:text-gray-400">Tidak ada café yang sesuai dengan filter lokasi Anda.</p>
<p class="mt-2 text-gray-500 dark:text-gray-500">Coba ubah filter lokasi atau gunakan pencarian tanpa filter.</p>
</div>
@else
@foreach($rankedCafes as $cafe)
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300 border border-gray-100 dark:border-gray-700">
@if($cafe->gambar)
<img src="{{ asset($cafe->gambar) }}" alt="{{ $cafe->nama }}" class="w-full h-52 object-cover">
@else
<div class="w-full h-52 bg-gradient-to-r from-gray-300 to-gray-200 dark:from-gray-700 dark:to-gray-600 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
@endif
<div class="p-5">
<h3 class="text-xl font-semibold text-gray-800 dark:text-white mb-2">{{ $cafe->nama }}</h3>
<p class="text-gray-600 dark:text-gray-300 mb-3 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block mr-1 text-gray-500 dark:text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1 1 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
{{ $cafe->lokasi }}
</p>
<div class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
<div class="text-sm text-gray-600 dark:text-gray-400">
<div class="font-medium mb-1">Kriteria:</div>
<ul class="space-y-1">
@foreach($cafe->ratings as $rating)
<li class="flex justify-between">
<span class="font-medium">{{ $rating->category->name }}:</span>
<!-- <span>{{ $rating->subcategory->name }} -->
<span class="text-yellow-500 ml-1">
@for ($i = 0; $i < $rating->subcategory->value; $i++)
@endfor
@for ($i = $rating->subcategory->value; $i < 5; $i++)
@endfor
</span>
<!-- </span> -->
</li>
@endforeach
</ul>
</div>
</div>
<!-- Tambahkan detail perhitungan skor -->
<div class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
<div class="text-sm text-gray-600 dark:text-gray-400">
<details class="group">
<summary class="font-medium mb-1 cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 flex items-center">
<span>Detail Perhitungan SMART</span>
<svg class="w-4 h-4 ml-1.5 transform transition-transform duration-200 group-open:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</summary>
<div class="mt-2 pl-2 text-xs bg-gray-50 dark:bg-gray-700 p-2 rounded-lg">
@if(isset($cafeScores[$cafe->id]['debug']) && !empty($cafeScores[$cafe->id]['debug']))
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr class="border-b dark:border-gray-700">
<th class="text-left py-1 px-2">Kriteria</th>
<th class="text-right py-1 px-2">Bobot (%)</th>
<th class="text-left py-1 px-2">Subkategori</th>
<th class="text-right py-1 px-2">Nilai</th>
<th class="text-right py-1 px-2">Skor</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
@php $totalScore = 0; @endphp
@foreach($cafeScores[$cafe->id]['debug'] as $category => $info)
<tr>
<td class="py-1 px-2">{{ $category }}</td>
<td class="text-right py-1 px-2">{{ number_format($info['bobot_normal'] * 100, 0) }}%</td>
<td class="text-left py-1 px-2">{{ $info['subcategory'] ?? 'N/A' }}</td>
<td class="text-right py-1 px-2">{{ number_format($info['nilai'], 1) }}</td>
<td class="text-right py-1 px-2">{{ number_format($info['skor_kriteria'], 2) }}</td>
</tr>
@php $totalScore += $info['skor_kriteria']; @endphp
@endforeach
</tbody>
<tfoot class="bg-gray-50 dark:bg-gray-700 font-medium">
<tr>
<td class="py-1 px-2">Total</td>
<td class="text-right py-1 px-2">100%</td>
<td class="text-left py-1 px-2"></td>
<td class="text-right py-1 px-2"></td>
<td class="text-right py-1 px-2">{{ number_format($totalScore, 2) }}</td>
</tr>
</tfoot>
</table>
@else
<p>Tidak ada detail perhitungan tersedia.</p>
@endif
</div>
</details>
</div>
</div>
<div class="mt-4 flex justify-between items-center">
<div class="text-lg font-bold text-blue-600 dark:text-blue-400">
Skor: {{ number_format($cafeScores[$cafe->id]['score'], 2) }}
</div>
<a href="#" class="text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition text-sm font-medium flex items-center">
Lihat Detail
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L12.586 11H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</a>
</div>
</div>
</div>
@endforeach
@endif
</div>
</div>
@else
<!-- Placeholder jika belum ada pencarian -->
<div id="resultsPlaceholder" class="text-center py-16 bg-white dark:bg-gray-800 rounded-xl shadow-md">
<svg xmlns="http://www.w3.org/2000/svg" class="h-20 w-20 mx-auto text-blue-400 dark:text-blue-500 mb-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 16l2.879-2.879m0 0a3 3 0 104.243-4.242 3 3 0 00-4.243 4.242zM21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h3 class="text-2xl font-medium text-gray-700 dark:text-gray-300">Mulai Pencarian Café</h3>
<p class="mt-3 text-gray-500 dark:text-gray-400 max-w-md mx-auto">Atur prioritas Anda dan pilih kriteria yang diinginkan, lalu klik "Cari Café Rekomendasi" untuk menemukan café yang sesuai dengan preferensi Anda.</p>
<button onclick="showSection1()" class="mt-6 px-6 py-2 bg-blue-500 text-white font-medium rounded-lg hover:bg-blue-600 transition-colors duration-300 shadow-md flex items-center mx-auto">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd" />
</svg>
Mulai Pencarian
</button>
</div>
@endif
</div>
</div>
</div>
@endsection

Some files were not shown because too many files have changed in this diff Show More