Merge branch 'feature-product' into development
WMS API/ECOMMERCE/pipeline/head This commit looks good
Details
WMS API/ECOMMERCE/pipeline/head This commit looks good
Details
This commit is contained in:
commit
12679dffe8
|
|
@ -51,6 +51,12 @@ return [
|
||||||
'notification' => 'Notification',
|
'notification' => 'Notification',
|
||||||
'confirm_delete_address' => 'Are you sure you want to delete this address?',
|
'confirm_delete_address' => 'Are you sure you want to delete this address?',
|
||||||
'this_action_cannot_be_undone' => 'This action cannot be undone.',
|
'this_action_cannot_be_undone' => 'This action cannot be undone.',
|
||||||
|
'select_location_on_map' => 'Select location on map',
|
||||||
|
'search_for_location' => 'Search for location',
|
||||||
|
'search' => 'Search',
|
||||||
|
'type_to_search' => 'Start typing to search for locations...',
|
||||||
|
'map_will_load_here' => 'Interactive map will load here when modal opens',
|
||||||
|
'drag_marker_to_adjust' => 'Drag the marker to adjust the exact location',
|
||||||
'regions' => [
|
'regions' => [
|
||||||
'africa' => 'Africa',
|
'africa' => 'Africa',
|
||||||
'asia' => 'Asia',
|
'asia' => 'Asia',
|
||||||
|
|
|
||||||
|
|
@ -68,4 +68,6 @@ return [
|
||||||
'shipping' => 'Shipping:',
|
'shipping' => 'Shipping:',
|
||||||
'estimated_total' => 'Estimated total:',
|
'estimated_total' => 'Estimated total:',
|
||||||
'proceed_to_checkout' => 'Proceed to checkout',
|
'proceed_to_checkout' => 'Proceed to checkout',
|
||||||
|
'order_summary' => 'Order summary',
|
||||||
|
'calculated_at_checkout' => 'Calculated at checkout',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -14,5 +14,8 @@ return [
|
||||||
'sign_up' => 'Sign Up',
|
'sign_up' => 'Sign Up',
|
||||||
'contact' => 'Contact',
|
'contact' => 'Contact',
|
||||||
'terms_conditions' => 'Terms & Conditions',
|
'terms_conditions' => 'Terms & Conditions',
|
||||||
'categories' => 'Categories'
|
'categories' => 'Categories',
|
||||||
|
'delivery' => 'Delivery',
|
||||||
|
'set_your_address' => 'Set your address',
|
||||||
|
'gender' => 'Gender',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,12 @@ return [
|
||||||
'confirm_delete_address' => 'Apakah Anda yakin ingin menghapus alamat ini?',
|
'confirm_delete_address' => 'Apakah Anda yakin ingin menghapus alamat ini?',
|
||||||
'this_action_cannot_be_undone' => 'Tindakan ini tidak dapat dibatalkan.',
|
'this_action_cannot_be_undone' => 'Tindakan ini tidak dapat dibatalkan.',
|
||||||
'notification' => 'Notifikasi',
|
'notification' => 'Notifikasi',
|
||||||
|
'select_location_on_map' => 'Pilih lokasi di peta',
|
||||||
|
'search_for_location' => 'Cari lokasi',
|
||||||
|
'search' => 'Cari',
|
||||||
|
'type_to_search' => 'Mulai mengetik untuk mencari lokasi...',
|
||||||
|
'map_will_load_here' => 'Peta interaktif akan dimuat di sini saat modal dibuka',
|
||||||
|
'drag_marker_to_adjust' => 'Seret penanda untuk menyesuaikan lokasi yang tepat',
|
||||||
'regions' => [
|
'regions' => [
|
||||||
'africa' => 'Afrika',
|
'africa' => 'Afrika',
|
||||||
'asia' => 'Asia',
|
'asia' => 'Asia',
|
||||||
|
|
|
||||||
|
|
@ -68,4 +68,6 @@ return [
|
||||||
'shipping' => 'Pengiriman:',
|
'shipping' => 'Pengiriman:',
|
||||||
'estimated_total' => 'Total estimasi:',
|
'estimated_total' => 'Total estimasi:',
|
||||||
'proceed_to_checkout' => 'Lanjut ke checkout',
|
'proceed_to_checkout' => 'Lanjut ke checkout',
|
||||||
|
'order_summary' => 'Ringkasan pesanan',
|
||||||
|
'calculated_at_checkout' => 'Dihitung di checkout',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,7 @@ return [
|
||||||
'contact' => 'Kontak',
|
'contact' => 'Kontak',
|
||||||
'terms_conditions' => 'Syarat & Ketentuan',
|
'terms_conditions' => 'Syarat & Ketentuan',
|
||||||
'categories' => 'Kategori',
|
'categories' => 'Kategori',
|
||||||
|
'delivery' => 'Pengiriman',
|
||||||
|
'set_your_address' => 'Atur alamat Anda',
|
||||||
|
'gender' => 'Gender',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -174,22 +174,19 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-6">
|
<div class="col-12">
|
||||||
<div class="position-relative">
|
<x-address.address-map
|
||||||
<label class="form-label">{{ __('addresses.latitude') }}</label>
|
:latitude="null"
|
||||||
<input type="text" class="form-control new-latitude" step="0.000001" required>
|
:longitude="null"
|
||||||
<div class="invalid-feedback">{{ __('addresses.please_enter_latitude') }}</div>
|
latitudeInputName="latitude"
|
||||||
</div>
|
longitudeInputName="longitude"
|
||||||
|
:searchPlaceholder="__('addresses.search_for_location')"
|
||||||
|
:searchButtonText="__('addresses.search')"
|
||||||
|
:mapLabel="__('addresses.select_location_on_map')"
|
||||||
|
:instructionText="__('addresses.drag_marker_to_adjust')"
|
||||||
|
:typeToSearchText="__('addresses.type_to_search')" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
<div class="col-sm-6">
|
|
||||||
<div class="position-relative">
|
|
||||||
<label class="form-label">{{ __('addresses.longitude') }}</label>
|
|
||||||
<input type="text" class="form-control new-longitude" step="0.000001" required>
|
|
||||||
<div class="invalid-feedback">{{ __('addresses.please_enter_longitude') }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<div class="position-relative">
|
<div class="position-relative">
|
||||||
<label for="new-address" class="form-label">{{ __('addresses.address') }}</label>
|
<label for="new-address" class="form-label">{{ __('addresses.address') }}</label>
|
||||||
<textarea class="form-control" id="new-address" rows="3" required></textarea>
|
<textarea class="form-control" id="new-address" rows="3" required></textarea>
|
||||||
|
|
@ -447,26 +444,37 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-6">
|
<div class="col-12">
|
||||||
<div class="position-relative">
|
<div class="address-map-component">
|
||||||
<label class="form-label">{{ __('addresses.latitude') }}</label>
|
<!-- Hidden inputs for latitude and longitude -->
|
||||||
<input type="text" class="form-control latitude" value="${address.latitude ?? ''}" step="0.000001" required>
|
<input type="hidden" name="latitude" id="latitude" value="${address.latitude ?? ''}" class="latitude address-latitude-input" step="0.000001">
|
||||||
<div class="invalid-feedback">{{ __('addresses.please_enter_latitude') }}</div>
|
<input type="hidden" name="longitude" id="longitude" value="${address.longitude ?? ''}" class="longitude address-longitude-input" step="0.000001">
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-sm-6">
|
<!-- Map interface -->
|
||||||
<div class="position-relative">
|
<div class="position-relative">
|
||||||
<label class="form-label">{{ __('addresses.longitude') }}</label>
|
<label class="form-label">{{ __('addresses.select_location_on_map') }}</label>
|
||||||
<input type="text" class="form-control longitude" value="${address.longitude ?? ''}" step="0.000001" required>
|
<div class="mb-3 position-relative">
|
||||||
<div class="invalid-feedback">{{ __('addresses.please_enter_longitude') }}</div>
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control address-location-search" placeholder="{{ __('addresses.search_for_location') }}" autocomplete="off">
|
||||||
|
<button class="btn btn-outline-secondary address-search-btn" type="button">
|
||||||
|
<i class="ci-search"></i> {{ __('addresses.search') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Search suggestions dropdown -->
|
||||||
|
<div class="address-search-suggestions position-absolute w-100 bg-white border rounded-top-0 rounded-bottom shadow-sm" style="z-index: 1050; max-height: 200px; overflow-y: auto; display: none;">
|
||||||
|
<div class="p-2 text-muted small">{{ __('addresses.type_to_search') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="address-map-container" style="height: 400px; width: 100%; border-radius: 8px; overflow: hidden;"></div>
|
||||||
|
<small class="text-muted">{{ __('addresses.drag_marker_to_adjust') }}</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-8">
|
<div class="col-12">
|
||||||
<div class="position-relative">
|
<div class="position-relative">
|
||||||
<label class="form-label">{{ __('addresses.address') }}</label>
|
<label class="form-label">{{ __('addresses.address') }}</label>
|
||||||
<input type="text" class="form-control address-input" value="${address.address}" required>
|
<textarea class="form-control address-input" rows="3" required>${address.address}</textarea>
|
||||||
<div class="invalid-feedback">{{ __('addresses.please_enter_address') }}</div>
|
<div class="invalid-feedback">{{ __('addresses.please_enter_address') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -523,6 +531,15 @@
|
||||||
// Load provinces and set initial values
|
// Load provinces and set initial values
|
||||||
loadProvinces(select, addressId, provinceId, cityId, districtId, villageId);
|
loadProvinces(select, addressId, provinceId, cityId, districtId, villageId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize map components (for dynamically generated content)
|
||||||
|
document.querySelectorAll('.address-map-component').forEach(component => {
|
||||||
|
// Trigger initialization for this component
|
||||||
|
if (window.initializeAddressMap) {
|
||||||
|
const componentIndex = Date.now() + Math.random();
|
||||||
|
window.initializeAddressMap(component, componentIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load addresses on page load
|
// Load addresses on page load
|
||||||
|
|
|
||||||
|
|
@ -42,12 +42,12 @@
|
||||||
</div> --}}
|
</div> --}}
|
||||||
|
|
||||||
<!-- Table of items -->
|
<!-- Table of items -->
|
||||||
<table class="table position-relative z-2 mb-4">
|
<table class="table z-2 mb-4">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="fs-sm fw-normal py-3 ps-0"><span
|
<th scope="col" class="fs-sm fw-normal py-3 ps-0"><span
|
||||||
class="text-body">Product</span></th>
|
class="text-body">Product</span></th>
|
||||||
|
|
||||||
<th scope="col" class="text-body fs-sm fw-normal py-3 d-none d-md-table-cell"><span
|
<th scope="col" class="text-body fs-sm fw-normal py-3 d-none d-md-table-cell"><span
|
||||||
class="text-body">Quantity</span></th>
|
class="text-body">Quantity</span></th>
|
||||||
<th scope="col" class="text-body fs-sm fw-normal py-3 d-none d-md-table-cell"><span
|
<th scope="col" class="text-body fs-sm fw-normal py-3 d-none d-md-table-cell"><span
|
||||||
|
|
@ -79,7 +79,9 @@
|
||||||
</tr>
|
</tr>
|
||||||
@else
|
@else
|
||||||
@foreach ($carts as $key => $cart)
|
@foreach ($carts as $key => $cart)
|
||||||
|
|
||||||
<tr data-cart-id="{{ $cart->id }}">
|
<tr data-cart-id="{{ $cart->id }}">
|
||||||
|
|
||||||
<td class="py-3 ps-0">
|
<td class="py-3 ps-0">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<a class="flex-shrink-0"
|
<a class="flex-shrink-0"
|
||||||
|
|
@ -88,7 +90,7 @@
|
||||||
width="80" alt="{{ $cart->name ?? 'Product' }}">
|
width="80" alt="{{ $cart->name ?? 'Product' }}">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
<div class="w-100 min-w-0 ps-2 ps-xl-3">
|
<div class="w-100 min-w-0 ps-2 ps-xl-3">
|
||||||
<h5 class="d-flex animate-underline mb-2">
|
<h5 class="d-flex animate-underline mb-2">
|
||||||
<a class="d-block fs-sm fw-medium text-truncate animate-target"
|
<a class="d-block fs-sm fw-medium text-truncate animate-target"
|
||||||
|
|
@ -105,9 +107,10 @@
|
||||||
</li>
|
</li>
|
||||||
</ul> --}}
|
</ul> --}}
|
||||||
|
|
||||||
<input type="hidden" id="price_{{ $cart->id }}"
|
<input type="hidden" id="price_{{ $cart->id }}"
|
||||||
value="{{ $cart->itemVariant->display_price ?? 0 }}">
|
value="{{ $cart->itemVariant->display_price ?? 0 }}">
|
||||||
Rp {{ number_format($cart->itemVariant->display_price ?? 0, 0, ',', '.') }}
|
Rp
|
||||||
|
{{ number_format($cart->itemVariant->display_price ?? 0, 0, ',', '.') }}
|
||||||
|
|
||||||
<div class="count-input rounded-2 d-md-none mt-3">
|
<div class="count-input rounded-2 d-md-none mt-3">
|
||||||
<button type="button" class="btn btn-sm btn-icon"
|
<button type="button" class="btn btn-sm btn-icon"
|
||||||
|
|
@ -124,7 +127,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="py-3 d-none d-md-table-cell">
|
<td class="py-3 d-none d-md-table-cell">
|
||||||
<div class="count-input">
|
<div class="count-input">
|
||||||
<button type="button" class="btn btn-icon" data-decrement
|
<button type="button" class="btn btn-icon" data-decrement
|
||||||
|
|
@ -177,7 +180,7 @@
|
||||||
<div class="position-sticky top-0" style="padding-top: 100px">
|
<div class="position-sticky top-0" style="padding-top: 100px">
|
||||||
<div class="bg-body-tertiary rounded-5 p-4 mb-3">
|
<div class="bg-body-tertiary rounded-5 p-4 mb-3">
|
||||||
<div class="p-sm-2 p-lg-0 p-xl-2">
|
<div class="p-sm-2 p-lg-0 p-xl-2">
|
||||||
<h5 class="border-bottom pb-4 mb-4">Order summary</h5>
|
<h5 class="border-bottom pb-4 mb-4">{{ __('checkout.order_summary') }}</h5>
|
||||||
<ul class="list-unstyled fs-sm gap-3 mb-0">
|
<ul class="list-unstyled fs-sm gap-3 mb-0">
|
||||||
<li class="d-flex justify-content-between">
|
<li class="d-flex justify-content-between">
|
||||||
<div>Subtotal (<span
|
<div>Subtotal (<span
|
||||||
|
|
@ -187,7 +190,8 @@
|
||||||
</li>
|
</li>
|
||||||
<li class="d-flex justify-content-between">
|
<li class="d-flex justify-content-between">
|
||||||
{{ __('checkout.saving') }}
|
{{ __('checkout.saving') }}
|
||||||
<span class="text-danger fw-medium">Rp {{ number_format(session('use_point') ?? 0,0,",",".") }}</span>
|
<span class="text-danger fw-medium">Rp
|
||||||
|
{{ number_format(session('use_point') ?? 0, 0, ',', '.') }}</span>
|
||||||
</li>
|
</li>
|
||||||
{{-- <li class="d-flex justify-content-between" id="tax-row" style="display: none;">
|
{{-- <li class="d-flex justify-content-between" id="tax-row" style="display: none;">
|
||||||
Tax collected:
|
Tax collected:
|
||||||
|
|
@ -195,7 +199,7 @@
|
||||||
</li> --}}
|
</li> --}}
|
||||||
<li class="d-flex justify-content-between">
|
<li class="d-flex justify-content-between">
|
||||||
{{ __('checkout.shipping') }}
|
{{ __('checkout.shipping') }}
|
||||||
<span class="text-dark-emphasis fw-medium">Calculated at checkout</span>
|
<span class="text-dark-emphasis fw-medium">{{ __('checkout.calculated_at_checkout') }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="border-top pt-4 mt-4">
|
<div class="border-top pt-4 mt-4">
|
||||||
|
|
@ -203,8 +207,7 @@
|
||||||
<span class="fs-sm">{{ __('checkout.estimated_total') }}</span>
|
<span class="fs-sm">{{ __('checkout.estimated_total') }}</span>
|
||||||
<span class="h5 mb-0" id="cart-estimated-total">$0.00</span>
|
<span class="h5 mb-0" id="cart-estimated-total">$0.00</span>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-lg btn-primary w-100"
|
<a class="btn btn-lg btn-primary w-100" href="{{ route('checkout.delivery') }}">
|
||||||
href="{{ route('checkout.delivery') }}">
|
|
||||||
{{ __('checkout.proceed_to_checkout') }}
|
{{ __('checkout.proceed_to_checkout') }}
|
||||||
<i class="ci-chevron-right fs-lg ms-1 me-n1"></i>
|
<i class="ci-chevron-right fs-lg ms-1 me-n1"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
@extends('layouts.landing', ['title' => 'Checkout'])
|
@extends('layouts.landing', ['title' => 'Checkout'])
|
||||||
|
|
||||||
|
@push('styles')
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||||
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||||
|
@endpush
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<x-layout.header-grocery />
|
<x-layout.header-grocery />
|
||||||
|
|
||||||
|
|
@ -128,6 +133,12 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<div id="address-map"
|
||||||
|
style="height: 300px; width: 100%; border-radius: 8px; overflow: hidden;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -191,8 +202,7 @@
|
||||||
<!-- Order summary (sticky sidebar) -->
|
<!-- Order summary (sticky sidebar) -->
|
||||||
<aside class="col-lg-4 offset-xl-1" style="margin-top: -100px">
|
<aside class="col-lg-4 offset-xl-1" style="margin-top: -100px">
|
||||||
<div class="position-sticky top-0" style="padding-top: 100px">
|
<div class="position-sticky top-0" style="padding-top: 100px">
|
||||||
<x-checkout.order-summary :subtotal="$subtotal" :total="$total"
|
<x-checkout.order-summary :subtotal="$subtotal" :total="$total" :showEdit="true" :editUrl="route('cart.index')" />
|
||||||
:showEdit="true" :editUrl="route('cart.index')" />
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -203,6 +213,8 @@
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('scripts')
|
@section('scripts')
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||||
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Delivery method selection
|
// Delivery method selection
|
||||||
|
|
@ -309,6 +321,9 @@
|
||||||
document.getElementById('longitude').textContent = selectedOption.dataset.longitude || '';
|
document.getElementById('longitude').textContent = selectedOption.dataset.longitude || '';
|
||||||
document.getElementById('zip').textContent = selectedOption.dataset.postcode || '';
|
document.getElementById('zip').textContent = selectedOption.dataset.postcode || '';
|
||||||
|
|
||||||
|
// Update map with coordinates
|
||||||
|
updateAddressMap(selectedOption.dataset.latitude, selectedOption.dataset.longitude);
|
||||||
|
|
||||||
// Update order summary with postcode
|
// Update order summary with postcode
|
||||||
if (selectedOption.dataset.postcode) {
|
if (selectedOption.dataset.postcode) {
|
||||||
updateOrderSummaryWithShipping();
|
updateOrderSummaryWithShipping();
|
||||||
|
|
@ -316,6 +331,69 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize map
|
||||||
|
let map = null;
|
||||||
|
let marker = null;
|
||||||
|
|
||||||
|
function initializeMap() {
|
||||||
|
if (typeof L !== 'undefined' && document.getElementById('address-map')) {
|
||||||
|
// Get initial coordinates from selected address or default to Jakarta
|
||||||
|
const addressSelect = document.getElementById('addressSelect');
|
||||||
|
let initialLat = -6.2088; // Default Jakarta latitude
|
||||||
|
let initialLng = 106.8456; // Default Jakarta longitude
|
||||||
|
let initialZoom = 13;
|
||||||
|
|
||||||
|
if (addressSelect && addressSelect.value && addressSelect.value !== 'new') {
|
||||||
|
const selectedOption = addressSelect.options[addressSelect.selectedIndex];
|
||||||
|
const lat = selectedOption.dataset.latitude;
|
||||||
|
const lng = selectedOption.dataset.longitude;
|
||||||
|
|
||||||
|
if (lat && lng) {
|
||||||
|
initialLat = parseFloat(lat);
|
||||||
|
initialLng = parseFloat(lng);
|
||||||
|
initialZoom = 15; // Zoom in closer for specific address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize map with address coordinates
|
||||||
|
map = L.map('address-map').setView([initialLat, initialLng], initialZoom);
|
||||||
|
|
||||||
|
// Add OpenStreetMap tiles
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: ' OpenStreetMap contributors'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Add initial marker if we have coordinates
|
||||||
|
if (initialLat !== -6.2088 || initialLng !== 106.8456) {
|
||||||
|
marker = L.marker([initialLat, initialLng]).addTo(map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAddressMap(lat, lng) {
|
||||||
|
if (!map || !lat || !lng) return;
|
||||||
|
|
||||||
|
const coordinates = [parseFloat(lat), parseFloat(lng)];
|
||||||
|
|
||||||
|
// Remove existing marker if any
|
||||||
|
if (marker) {
|
||||||
|
map.removeLayer(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new marker
|
||||||
|
marker = L.marker(coordinates).addTo(map);
|
||||||
|
|
||||||
|
// Center map on new coordinates
|
||||||
|
map.setView(coordinates, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAddressMap() {
|
||||||
|
if (marker && map) {
|
||||||
|
map.removeLayer(marker);
|
||||||
|
marker = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Populate on page load
|
// Populate on page load
|
||||||
populateAddressForm();
|
populateAddressForm();
|
||||||
// Set initial address ID
|
// Set initial address ID
|
||||||
|
|
@ -338,33 +416,22 @@
|
||||||
document.getElementById('latitude').textContent = '';
|
document.getElementById('latitude').textContent = '';
|
||||||
document.getElementById('longitude').textContent = '';
|
document.getElementById('longitude').textContent = '';
|
||||||
document.getElementById('zip').textContent = '';
|
document.getElementById('zip').textContent = '';
|
||||||
|
|
||||||
|
// Clear map
|
||||||
|
clearAddressMap();
|
||||||
|
|
||||||
// Clear address ID input
|
// Clear address ID input
|
||||||
document.getElementById('addressIdInput').value = '';
|
document.getElementById('addressIdInput').value = '';
|
||||||
} else {
|
} else {
|
||||||
// Populate form fields with selected address
|
// Populate form fields with selected address
|
||||||
document.getElementById('firstName').textContent = selectedOption.dataset
|
populateAddressForm();
|
||||||
.firstName || '';
|
|
||||||
// document.getElementById('lastName').value = selectedOption.dataset.lastName || '';
|
|
||||||
document.getElementById('address').textContent = selectedOption.dataset.address ||
|
|
||||||
'';
|
|
||||||
document.getElementById('city').textContent = selectedOption.dataset.city || '';
|
|
||||||
document.getElementById('state').textContent = selectedOption.dataset.state || '';
|
|
||||||
document.getElementById('zip').textContent = selectedOption.dataset.postcode || '';
|
|
||||||
document.getElementById('phone').textContent = selectedOption.dataset.phone || '';
|
|
||||||
document.getElementById('latitude').textContent = selectedOption.dataset.latitude ||
|
|
||||||
'';
|
|
||||||
document.getElementById('longitude').textContent = selectedOption.dataset
|
|
||||||
.longitude || '';
|
|
||||||
document.getElementById('zip').textContent = selectedOption.dataset.postcode || '';
|
|
||||||
// Update address ID input
|
// Update address ID input
|
||||||
document.getElementById('addressIdInput').value = this.value;
|
document.getElementById('addressIdInput').value = this.value;
|
||||||
|
|
||||||
// Update order summary with postcode
|
|
||||||
if (selectedOption.dataset.postcode) {
|
|
||||||
updateOrderSummaryWithShipping();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize map on page load
|
||||||
|
initializeMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-update order summary when postcode changes
|
// Auto-update order summary when postcode changes
|
||||||
|
|
@ -482,7 +549,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function showShippingCost(cost, isFree = false) {
|
function showShippingCost(cost, isFree = false) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateDeliveryForm() {
|
function validateDeliveryForm() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,438 @@
|
||||||
|
@props([
|
||||||
|
'latitude' => null,
|
||||||
|
'longitude' => null,
|
||||||
|
'latitudeInputName' => 'latitude',
|
||||||
|
'longitudeInputName' => 'longitude',
|
||||||
|
'searchPlaceholder' => 'Search for location',
|
||||||
|
'searchButtonText' => 'Search',
|
||||||
|
'mapLabel' => 'Select location on map',
|
||||||
|
'instructionText' => 'Drag the marker to adjust the exact location',
|
||||||
|
'typeToSearchText' => 'Start typing to search for locations...',
|
||||||
|
])
|
||||||
|
|
||||||
|
<div class="address-map-component">
|
||||||
|
<!-- Hidden inputs for latitude and longitude -->
|
||||||
|
<input type="hidden" name="{{ $latitudeInputName }}" id="{{ $latitudeInputName }}" value="{{ $latitude ?? '' }}"
|
||||||
|
class="address-latitude-input" step="0.000001">
|
||||||
|
|
||||||
|
<input type="hidden" name="{{ $longitudeInputName }}" id="{{ $longitudeInputName }}" value="{{ $longitude ?? '' }}"
|
||||||
|
class="address-longitude-input" step="0.000001">
|
||||||
|
|
||||||
|
<!-- Map interface -->
|
||||||
|
<div class="position-relative">
|
||||||
|
<label class="form-label">{{ $mapLabel }}</label>
|
||||||
|
<div class="mb-3 position-relative">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control address-location-search"
|
||||||
|
placeholder="{{ $searchPlaceholder }}" autocomplete="off">
|
||||||
|
<button class="btn btn-outline-secondary address-search-btn" type="button">
|
||||||
|
<i class="ci-search"></i> {{ $searchButtonText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Search suggestions dropdown -->
|
||||||
|
<div class="address-search-suggestions position-absolute w-100 bg-white border rounded-top-0 rounded-bottom shadow-sm"
|
||||||
|
style="z-index: 1050; max-height: 200px; overflow-y: auto; display: none;">
|
||||||
|
<div class="p-2 text-muted small">{{ $typeToSearchText }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="address-map-container" style="height: 400px; width: 100%; border-radius: 8px; overflow: hidden;">
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">{{ $instructionText }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||||
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||||
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize all address map components
|
||||||
|
const addressMapComponents = document.querySelectorAll('.address-map-component');
|
||||||
|
|
||||||
|
addressMapComponents.forEach(function(component, index) {
|
||||||
|
initializeAddressMap(component, index);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize maps from placeholders (for dynamically generated content)
|
||||||
|
function initializeMapFromPlaceholder(placeholder) {
|
||||||
|
const component = createMapComponentFromPlaceholder(placeholder);
|
||||||
|
if (component) {
|
||||||
|
initializeAddressMap(component, Date.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapComponentFromPlaceholder(placeholder) {
|
||||||
|
// Get data attributes from placeholder
|
||||||
|
const latitude = placeholder.dataset.latitude;
|
||||||
|
const longitude = placeholder.dataset.longitude;
|
||||||
|
const latitudeInputName = placeholder.dataset.latitudeInput || 'latitude';
|
||||||
|
const longitudeInputName = placeholder.dataset.longitudeInput || 'longitude';
|
||||||
|
const searchPlaceholder = placeholder.dataset.searchPlaceholder || 'Search for location';
|
||||||
|
const searchButtonText = placeholder.dataset.searchButton || 'Search';
|
||||||
|
const mapLabel = placeholder.dataset.mapLabel || 'Select location on map';
|
||||||
|
const instructionText = placeholder.dataset.instructionText ||
|
||||||
|
'Drag the marker to adjust the exact location';
|
||||||
|
const typeToSearchText = placeholder.dataset.typeToSearch ||
|
||||||
|
'Start typing to search for locations...';
|
||||||
|
|
||||||
|
// Create component HTML
|
||||||
|
const componentHtml = `
|
||||||
|
<div class="address-map-component">
|
||||||
|
<!-- Hidden inputs for latitude and longitude -->
|
||||||
|
<input type="hidden"
|
||||||
|
name="${latitudeInputName}"
|
||||||
|
id="${latitudeInputName}"
|
||||||
|
value="${latitude || ''}"
|
||||||
|
class="address-latitude-input"
|
||||||
|
step="0.000001">
|
||||||
|
|
||||||
|
<input type="hidden"
|
||||||
|
name="${longitudeInputName}"
|
||||||
|
id="${longitudeInputName}"
|
||||||
|
value="${longitude || ''}"
|
||||||
|
class="address-longitude-input"
|
||||||
|
step="0.000001">
|
||||||
|
|
||||||
|
<!-- Map interface -->
|
||||||
|
<div class="position-relative">
|
||||||
|
<label class="form-label">${mapLabel}</label>
|
||||||
|
<div class="mb-3 position-relative">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text"
|
||||||
|
class="form-control address-location-search"
|
||||||
|
placeholder="${searchPlaceholder}"
|
||||||
|
autocomplete="off">
|
||||||
|
<button class="btn btn-outline-secondary address-search-btn" type="button">
|
||||||
|
<i class="ci-search"></i> ${searchButtonText}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Search suggestions dropdown -->
|
||||||
|
<div class="address-search-suggestions position-absolute w-100 bg-white border rounded-top-0 rounded-bottom shadow-sm"
|
||||||
|
style="z-index: 1050; max-height: 200px; overflow-y: auto; display: none;">
|
||||||
|
<div class="p-2 text-muted small">${typeToSearchText}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="address-map-container" style="height: 400px; width: 100%; border-radius: 8px; overflow: hidden;"></div>
|
||||||
|
<small class="text-muted">${instructionText}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Replace placeholder with component
|
||||||
|
placeholder.outerHTML = componentHtml;
|
||||||
|
|
||||||
|
// Return the new component element
|
||||||
|
return placeholder.parentElement.querySelector('.address-map-component');
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeAddressMap(component, componentIndex) {
|
||||||
|
let map = null;
|
||||||
|
let marker = null;
|
||||||
|
let searchTimeout;
|
||||||
|
|
||||||
|
const mapContainer = component.querySelector('.address-map-container');
|
||||||
|
const searchInput = component.querySelector('.address-location-search');
|
||||||
|
const searchBtn = component.querySelector('.address-search-btn');
|
||||||
|
const suggestionsDiv = component.querySelector('.address-search-suggestions');
|
||||||
|
const latInput = component.querySelector('.address-latitude-input');
|
||||||
|
const lngInput = component.querySelector('.address-longitude-input');
|
||||||
|
|
||||||
|
// Generate unique IDs for this component
|
||||||
|
const mapId = 'address-map-' + componentIndex;
|
||||||
|
mapContainer.id = mapId;
|
||||||
|
|
||||||
|
// Initialize map
|
||||||
|
function initializeMap() {
|
||||||
|
if (typeof L !== 'undefined' && mapContainer) {
|
||||||
|
// Get initial coordinates or default to Jakarta
|
||||||
|
let initialLat = -6.2088;
|
||||||
|
let initialLng = 106.8456;
|
||||||
|
let initialZoom = 13;
|
||||||
|
|
||||||
|
if (latInput.value && lngInput.value) {
|
||||||
|
const lat = parseFloat(latInput.value);
|
||||||
|
const lng = parseFloat(lngInput.value);
|
||||||
|
if (!isNaN(lat) && !isNaN(lng)) {
|
||||||
|
initialLat = lat;
|
||||||
|
initialLng = lng;
|
||||||
|
initialZoom = 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize map
|
||||||
|
map = L.map(mapId).setView([initialLat, initialLng], initialZoom);
|
||||||
|
|
||||||
|
// Add OpenStreetMap tiles
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: ' OpenStreetMap contributors'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Add initial marker if coordinates exist
|
||||||
|
if (initialLat !== -6.2088 || initialLng !== 106.8456) {
|
||||||
|
marker = L.marker([initialLat, initialLng], {
|
||||||
|
draggable: true
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Add drag event listener
|
||||||
|
marker.on('dragend', function(e) {
|
||||||
|
const position = e.target.getLatLng();
|
||||||
|
latInput.value = position.lat.toFixed(6);
|
||||||
|
lngInput.value = position.lng.toFixed(6);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Add draggable marker at center
|
||||||
|
marker = L.marker([-6.2088, 106.8456], {
|
||||||
|
draggable: true
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Add drag event listener
|
||||||
|
marker.on('dragend', function(e) {
|
||||||
|
const position = e.target.getLatLng();
|
||||||
|
latInput.value = position.lat.toFixed(6);
|
||||||
|
lngInput.value = position.lng.toFixed(6);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add click event to place marker
|
||||||
|
map.on('click', function(e) {
|
||||||
|
const position = e.latlng;
|
||||||
|
|
||||||
|
if (marker) {
|
||||||
|
marker.setLatLng(position);
|
||||||
|
} else {
|
||||||
|
marker = L.marker(position, {
|
||||||
|
draggable: true
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Add drag event listener
|
||||||
|
marker.on('dragend', function(e) {
|
||||||
|
const pos = e.target.getLatLng();
|
||||||
|
latInput.value = pos.lat.toFixed(6);
|
||||||
|
lngInput.value = pos.lng.toFixed(6);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
latInput.value = position.lat.toFixed(6);
|
||||||
|
lngInput.value = position.lng.toFixed(6);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search functionality
|
||||||
|
function searchLocation(query) {
|
||||||
|
if (!query) {
|
||||||
|
console.log('Empty query, skipping search');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure map is initialized
|
||||||
|
if (!map) {
|
||||||
|
console.log('Map not initialized, trying to initialize...');
|
||||||
|
initializeMap();
|
||||||
|
// Wait a bit for map to initialize
|
||||||
|
setTimeout(() => {
|
||||||
|
if (map) {
|
||||||
|
performSearch(query);
|
||||||
|
} else {
|
||||||
|
console.error('Map still not initialized after retry');
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
performSearch(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
function performSearch(query) {
|
||||||
|
console.log('Performing search for:', query);
|
||||||
|
|
||||||
|
// Using Nominatim API for geocoding
|
||||||
|
fetch(
|
||||||
|
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=5&viewbox=95,-11,141,6&bounded=1&namedetails=1&addressdetails=1`)
|
||||||
|
.then(response => {
|
||||||
|
console.log('Search response received:', response.status);
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
console.log('Search results:', data);
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
displaySearchSuggestions(data);
|
||||||
|
} else {
|
||||||
|
displaySearchSuggestions([]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Search error:', error);
|
||||||
|
displaySearchSuggestions([]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displaySearchSuggestions(results) {
|
||||||
|
console.log('Displaying suggestions:', results);
|
||||||
|
|
||||||
|
if (!suggestionsDiv) {
|
||||||
|
console.error('Suggestions div not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
suggestionsDiv.innerHTML = '<div class="p-2 text-muted small">No locations found</div>';
|
||||||
|
suggestionsDiv.style.display = 'block';
|
||||||
|
console.log('No results found, showing message');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
results.forEach(result => {
|
||||||
|
const displayName = result.display_name || result.name;
|
||||||
|
html += `
|
||||||
|
<div class="suggestion-item p-2 border-bottom hover-bg-light cursor-pointer"
|
||||||
|
data-lat="${result.lat}" data-lng="${result.lon}" data-name="${displayName}">
|
||||||
|
<div class="fw-medium">${result.name}</div>
|
||||||
|
<div class="text-muted small">${displayName}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
suggestionsDiv.innerHTML = html;
|
||||||
|
suggestionsDiv.style.display = 'block';
|
||||||
|
console.log('Suggestions displayed, HTML length:', html.length);
|
||||||
|
|
||||||
|
// Add click handlers to suggestions
|
||||||
|
suggestionsDiv.querySelectorAll('.suggestion-item').forEach(item => {
|
||||||
|
item.addEventListener('click', function() {
|
||||||
|
const lat = parseFloat(this.dataset.lat);
|
||||||
|
const lng = parseFloat(this.dataset.lng);
|
||||||
|
const name = this.dataset.name;
|
||||||
|
|
||||||
|
console.log('Suggestion clicked:', {
|
||||||
|
lat,
|
||||||
|
lng,
|
||||||
|
name
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update map view
|
||||||
|
map.setView([lat, lng], 15);
|
||||||
|
|
||||||
|
// Update or create marker
|
||||||
|
if (marker) {
|
||||||
|
marker.setLatLng([lat, lng]);
|
||||||
|
} else {
|
||||||
|
marker = L.marker([lat, lng], {
|
||||||
|
draggable: true
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Add drag event listener
|
||||||
|
marker.on('dragend', function(e) {
|
||||||
|
const position = e.target.getLatLng();
|
||||||
|
latInput.value = position.lat.toFixed(6);
|
||||||
|
lngInput.value = position.lng.toFixed(6);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update input fields
|
||||||
|
latInput.value = lat.toFixed(6);
|
||||||
|
lngInput.value = lng.toFixed(6);
|
||||||
|
searchInput.value = name;
|
||||||
|
|
||||||
|
// Hide suggestions
|
||||||
|
suggestionsDiv.style.display = 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideSuggestions() {
|
||||||
|
if (suggestionsDiv) {
|
||||||
|
suggestionsDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
if (searchBtn && searchInput) {
|
||||||
|
console.log('Search elements found:', {
|
||||||
|
searchBtn,
|
||||||
|
searchInput
|
||||||
|
});
|
||||||
|
|
||||||
|
searchBtn.addEventListener('click', function() {
|
||||||
|
console.log('Search button clicked, value:', searchInput.value);
|
||||||
|
searchLocation(searchInput.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
searchInput.addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
console.log('Enter key pressed, value:', searchInput.value);
|
||||||
|
e.preventDefault();
|
||||||
|
hideSuggestions();
|
||||||
|
searchLocation(searchInput.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add debounced search for real-time suggestions
|
||||||
|
searchInput.addEventListener('input', function(e) {
|
||||||
|
const query = e.target.value.trim();
|
||||||
|
console.log('Input event, query:', query);
|
||||||
|
|
||||||
|
// Clear previous timeout
|
||||||
|
if (searchTimeout) {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.length < 2) {
|
||||||
|
hideSuggestions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce search (wait 300ms after user stops typing)
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
console.log('Debounced search triggered for:', query);
|
||||||
|
searchLocation(query);
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide suggestions when clicking outside
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (!e.target.closest('.address-map-component')) {
|
||||||
|
hideSuggestions();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide suggestions on ESC key
|
||||||
|
searchInput.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
hideSuggestions();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Search elements not found:', {
|
||||||
|
searchBtn,
|
||||||
|
searchInput
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update marker when coordinates are manually entered
|
||||||
|
[latInput, lngInput].forEach(input => {
|
||||||
|
input.addEventListener('change', function() {
|
||||||
|
const lat = parseFloat(latInput.value);
|
||||||
|
const lng = parseFloat(lngInput.value);
|
||||||
|
|
||||||
|
if (!isNaN(lat) && !isNaN(lng) && map && marker) {
|
||||||
|
marker.setLatLng([lat, lng]);
|
||||||
|
map.setView([lat, lng], 15);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize map when component is ready
|
||||||
|
setTimeout(() => {
|
||||||
|
initializeMap();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make functions globally available for dynamic initialization
|
||||||
|
window.initializeMapFromPlaceholder = initializeMapFromPlaceholder;
|
||||||
|
window.initializeAddressMap = initializeAddressMap;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
<button type="button" class="btn btn-lg btn-secondary w-100 border-0 rounded-pill" data-bs-toggle="dropdown"
|
<button type="button" class="btn btn-lg btn-secondary w-100 border-0 rounded-pill" data-bs-toggle="dropdown"
|
||||||
aria-haspopup="true" aria-expanded="false">
|
aria-haspopup="true" aria-expanded="false">
|
||||||
<i class="ci-grid fs-lg me-2 ms-n1"></i>
|
<i class="ci-grid fs-lg me-2 ms-n1"></i>
|
||||||
Categories
|
{{ __('navbar.categories') }}
|
||||||
<i class="ci-chevron-down fs-lg me-2 ms-auto me-n1"></i>
|
<i class="ci-chevron-down fs-lg me-2 ms-auto me-n1"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu rounded-4 p-4" style="--cz-dropdown-spacer: .75rem; margin-left: -75px">
|
<div class="dropdown-menu rounded-4 p-4" style="--cz-dropdown-spacer: .75rem; margin-left: -75px">
|
||||||
<div class="d-flex gap-4">
|
<div class="d-flex gap-4">
|
||||||
<div style="min-width: 170px">
|
<div style="min-width: 170px">
|
||||||
<div class="h6">Kategori</div>
|
<div class="h6">{{ __('navbar.categories') }}</div>
|
||||||
<ul class="nav flex-column gap-2 mt-n2">
|
<ul class="nav flex-column gap-2 mt-n2">
|
||||||
|
|
||||||
@foreach ($categories as $category)
|
@foreach ($categories as $category)
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div style="min-width: 170px">
|
<div style="min-width: 170px">
|
||||||
<div class="h6">Gender</div>
|
<div class="h6">{{ __('navbar.gender') }}</div>
|
||||||
<ul class="nav flex-column gap-2 mt-n2">
|
<ul class="nav flex-column gap-2 mt-n2">
|
||||||
@foreach ($genders as $gender)
|
@foreach ($genders as $gender)
|
||||||
<li class="d-flex w-100 pt-1">
|
<li class="d-flex w-100 pt-1">
|
||||||
|
|
|
||||||
|
|
@ -57,9 +57,9 @@
|
||||||
|
|
||||||
<div class="nav me-4 me-xxl-5 d-none d-xl-block">
|
<div class="nav me-4 me-xxl-5 d-none d-xl-block">
|
||||||
<a class="nav-link flex-column align-items-start animate-underline p-0" href="{{ route('addresses') }}">
|
<a class="nav-link flex-column align-items-start animate-underline p-0" href="{{ route('addresses') }}">
|
||||||
<div class="h6 fs-sm mb-0">Delivery</div>
|
<div class="h6 fs-sm mb-0">{{ __('navbar.delivery') }}</div>
|
||||||
<div class="d-flex align-items-center fs-sm fw-normal text-body">
|
<div class="d-flex align-items-center fs-sm fw-normal text-body">
|
||||||
<span class="animate-target text-nowrap">{{ auth()->user()->primary_address->label ?? 'Set your address' }}</span>
|
<span class="animate-target text-nowrap">{{ auth()->user()->primary_address->label ?? __('navbar.set_your_address') }}</span>
|
||||||
<i class="ci-chevron-down fs-base ms-1"></i>
|
<i class="ci-chevron-down fs-base ms-1"></i>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -99,15 +99,12 @@
|
||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
<h1 class="h3">{{ $product->name }}</h1>
|
<h1 class="h3">{{ $product->name }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{{-- wishlist/love button --}}
|
{{-- wishlist/love button --}}
|
||||||
<button type="button"
|
<button type="button" class="btn btn-outline-danger gap-2" style="height: 50px;"
|
||||||
class="btn btn-outline-danger gap-2"
|
id="wishlist-btn" data-item-id="{{ $product->id }}"
|
||||||
style="height: 50px;"
|
data-is-wishlist="{{ $product->isWishlist() ? 'true' : 'false' }}">
|
||||||
id="wishlist-btn"
|
|
||||||
data-item-id="{{ $product->id }}"
|
|
||||||
data-is-wishlist="{{ $product->isWishlist() ? 'true' : 'false' }}">
|
|
||||||
<i class="{{ $product->isWishlist() == true ? 'ci-heart-filled' : 'ci-heart' }}"></i>
|
<i class="{{ $product->isWishlist() == true ? 'ci-heart-filled' : 'ci-heart' }}"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -118,12 +115,14 @@
|
||||||
class="text-decoration-none text-body-emphasis animate-underline text-xs border border-secondary rounded-pill px-2 py-1"
|
class="text-decoration-none text-body-emphasis animate-underline text-xs border border-secondary rounded-pill px-2 py-1"
|
||||||
style="font-size:10pt;">{{ $product->category->name }}</a>
|
style="font-size:10pt;">{{ $product->category->name }}</a>
|
||||||
|
|
||||||
<a href="{{ route('product.index', ['filter[brand_id]' => $product->brand->id]) }}"
|
@if ($product->brand?->id != null)
|
||||||
class="text-decoration-none text-body-emphasis animate-underline text-xs border border-secondary rounded-pill px-2 py-1"
|
<a href="{{ route('product.index', ['filter[brand_id]' => $product->brand->id]) }}"
|
||||||
style="font-size:10pt;">{{ $product->brand->name }}</a>
|
class="text-decoration-none text-body-emphasis animate-underline text-xs border border-secondary rounded-pill px-2 py-1"
|
||||||
|
style="font-size:10pt;">{{ $product->brand->name }}</a>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<p class="fs-sm mb-0">{!! nl2br(Str::limit($product->description, 500)) !!}</p>
|
<p class="fs-sm mb-0">{!! nl2br(Str::limit($product->description, 500)) !!}</p>
|
||||||
|
|
@ -209,8 +208,9 @@
|
||||||
</div> --}}
|
</div> --}}
|
||||||
|
|
||||||
<!-- Count input + Add to cart button -->
|
<!-- Count input + Add to cart button -->
|
||||||
<x-shop.add-to-cart :item_reference_id="$product->variants->first()->id" />
|
@if ($product->variants->first()->reference)
|
||||||
|
<x-shop.add-to-cart :item_reference_id="$product->variants->first()->reference->id" />
|
||||||
|
@endif
|
||||||
<!-- Info list -->
|
<!-- Info list -->
|
||||||
{{-- <ul class="list-unstyled gap-3 pb-3 pb-lg-4 mb-3">
|
{{-- <ul class="list-unstyled gap-3 pb-3 pb-lg-4 mb-3">
|
||||||
<li class="d-flex flex-wrap fs-sm">
|
<li class="d-flex flex-wrap fs-sm">
|
||||||
|
|
@ -1061,14 +1061,14 @@
|
||||||
discountSpan.style.display = 'none';
|
discountSpan.style.display = 'none';
|
||||||
}
|
}
|
||||||
document.querySelector('.product-main-image').src = image;
|
document.querySelector('.product-main-image').src = image;
|
||||||
document.querySelector('.variantOption').textContent = labelText;
|
// document.querySelector('.variantOption').textContent = labelText;
|
||||||
|
|
||||||
// Update AddToCart component with new variant ID
|
// Update AddToCart component with new variant ID
|
||||||
const addToCartBtn = document.querySelector('[data-item-id]');
|
const addToCartBtn = document.querySelector('[data-item-reference-id]');
|
||||||
if (addToCartBtn) {
|
if (addToCartBtn) {
|
||||||
console.log(this.value);
|
console.log(this.value);
|
||||||
var item_id = this.value;
|
var item_id = this.value;
|
||||||
addToCartBtn.setAttribute('data-item-id', item_id);
|
addToCartBtn.setAttribute('data-item-reference-id', item_id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1088,95 +1088,100 @@
|
||||||
const isWishlist = this.getAttribute('data-is-wishlist') === 'true';
|
const isWishlist = this.getAttribute('data-is-wishlist') === 'true';
|
||||||
const icon = this.querySelector('i');
|
const icon = this.querySelector('i');
|
||||||
const text = this.querySelector('span');
|
const text = this.querySelector('span');
|
||||||
|
|
||||||
// Get CSRF token
|
// Get CSRF token
|
||||||
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute(
|
||||||
|
'content');
|
||||||
|
|
||||||
if (isWishlist) {
|
if (isWishlist) {
|
||||||
// Remove from wishlist
|
// Remove from wishlist
|
||||||
fetch('/account/wishlist/' + itemId, {
|
fetch('/account/wishlist/' + itemId, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-CSRF-TOKEN': token,
|
'X-CSRF-TOKEN': token,
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
// Update button state
|
// Update button state
|
||||||
this.setAttribute('data-is-wishlist', 'false');
|
this.setAttribute('data-is-wishlist', 'false');
|
||||||
icon.className = 'ci-heart';
|
icon.className = 'ci-heart';
|
||||||
text.textContent = 'Add to Wishlist';
|
text.textContent = 'Add to Wishlist';
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
const successAlert = document.createElement('div');
|
const successAlert = document.createElement('div');
|
||||||
successAlert.className = 'alert alert-success alert-dismissible fade show position-fixed';
|
successAlert.className =
|
||||||
successAlert.style.css = 'top: 20px; right: 20px; z-index: 9999; max-width: 300px;';
|
'alert alert-success alert-dismissible fade show position-fixed';
|
||||||
successAlert.innerHTML = `
|
successAlert.style.css =
|
||||||
|
'top: 20px; right: 20px; z-index: 9999; max-width: 300px;';
|
||||||
|
successAlert.innerHTML = `
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert">×</button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert">×</button>
|
||||||
Item removed from wishlist!
|
Item removed from wishlist!
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(successAlert);
|
document.body.appendChild(successAlert);
|
||||||
|
|
||||||
// Auto-hide after 3 seconds
|
// Auto-hide after 3 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (successAlert.parentNode) {
|
if (successAlert.parentNode) {
|
||||||
successAlert.parentNode.removeChild(successAlert);
|
successAlert.parentNode.removeChild(successAlert);
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to remove from wishlist:', data.message);
|
console.error('Failed to remove from wishlist:', data.message);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error removing from wishlist:', error);
|
console.error('Error removing from wishlist:', error);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Add to wishlist
|
// Add to wishlist
|
||||||
fetch('/account/wishlist', {
|
fetch('/account/wishlist', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-CSRF-TOKEN': token,
|
'X-CSRF-TOKEN': token,
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
item_id: itemId
|
item_id: itemId
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
.then(response => response.json())
|
||||||
.then(response => response.json())
|
.then(data => {
|
||||||
.then(data => {
|
if (data.success) {
|
||||||
if (data.success) {
|
// Update button state
|
||||||
// Update button state
|
this.setAttribute('data-is-wishlist', 'true');
|
||||||
this.setAttribute('data-is-wishlist', 'true');
|
icon.className = 'ci-heart-filled';
|
||||||
icon.className = 'ci-heart-filled';
|
text.textContent = 'In Wishlist';
|
||||||
text.textContent = 'In Wishlist';
|
|
||||||
|
// Show success message
|
||||||
// Show success message
|
const successAlert = document.createElement('div');
|
||||||
const successAlert = document.createElement('div');
|
successAlert.className =
|
||||||
successAlert.className = 'alert alert-success alert-dismissible fade show position-fixed';
|
'alert alert-success alert-dismissible fade show position-fixed';
|
||||||
successAlert.style.css = 'top: 20px; right: 20px; z-index: 9999; max-width: 300px;';
|
successAlert.style.css =
|
||||||
successAlert.innerHTML = `
|
'top: 20px; right: 20px; z-index: 9999; max-width: 300px;';
|
||||||
|
successAlert.innerHTML = `
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert">×</button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert">×</button>
|
||||||
Item added to wishlist!
|
Item added to wishlist!
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(successAlert);
|
document.body.appendChild(successAlert);
|
||||||
|
|
||||||
// Auto-hide after 3 seconds
|
// Auto-hide after 3 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (successAlert.parentNode) {
|
if (successAlert.parentNode) {
|
||||||
successAlert.parentNode.removeChild(successAlert);
|
successAlert.parentNode.removeChild(successAlert);
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to add to wishlist:', data.message);
|
console.error('Failed to add to wishlist:', data.message);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error adding to wishlist:', error);
|
console.error('Error adding to wishlist:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue