load address using ajax

This commit is contained in:
Bayu Lukman Yusuf 2026-01-20 16:31:17 +07:00
parent 10f3abc047
commit a86ba91db7
4 changed files with 418 additions and 171 deletions

View File

@ -13,7 +13,32 @@ class AddressController extends Controller
{ {
public function index(Request $request) public function index(Request $request)
{ {
$addresses = auth()->user()->addresses; $addresses = auth()->user()->addresses()->orderBy('is_primary', 'desc')->get();
// If AJAX request, return JSON
if ($request->ajax() || $request->wantsJson()) {
$addressesData = $addresses->map(function ($address) {
return [
'id' => $address->id,
'label' => $address->label,
'location' => $address->location,
'address' => $address->address,
'is_primary' => $address->is_primary,
'province_id' => $address->province_id,
'city_id' => $address->city_id,
'district_id' => $address->district_id,
'subdistrict_id' => $address->subdistrict_id,
'postal_code' => $address->postal_code,
];
});
return response()->json([
'success' => true,
'addresses' => $addressesData
]);
}
// For regular page load, return view
return view('account.addresses', compact('addresses')); return view('account.addresses', compact('addresses'));
} }

View File

@ -37,6 +37,8 @@ return [
'cannot_delete_only_address' => 'Cannot delete the only address', 'cannot_delete_only_address' => 'Cannot delete the only address',
'delete' => 'Delete', 'delete' => 'Delete',
'cancel' => 'Cancel', 'cancel' => 'Cancel',
'loading' => 'Loading...',
'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.',
'regions' => [ 'regions' => [

View File

@ -37,8 +37,10 @@ return [
'cannot_delete_only_address' => 'Tidak dapat menghapus satu-satunya alamat', 'cannot_delete_only_address' => 'Tidak dapat menghapus satu-satunya alamat',
'delete' => 'Hapus', 'delete' => 'Hapus',
'cancel' => 'Batal', 'cancel' => 'Batal',
'loading' => 'Memuat...',
'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',
'regions' => [ 'regions' => [
'africa' => 'Afrika', 'africa' => 'Afrika',
'asia' => 'Asia', 'asia' => 'Asia',

View File

@ -1,6 +1,53 @@
@extends('layouts.account', ['title' => __('addresses.page_title')]) @extends('layouts.account', ['title' => __('addresses.page_title')])
@section('content') @section('content')
<style>
.shimmer-wrapper {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 100%;
margin: 0 auto;
}
.shimmer {
background: #f0f0f0;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid #e9ecef;
}
.shimmer-line {
height: 0.75rem;
background: linear-gradient(90deg, #f0f0f0 25%, #e9ecef 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 0.25rem;
margin-bottom: 0.5rem;
}
.shimmer-line.short {
width: 60%;
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
#addresses-loading {
transition: opacity 0.3s ease;
}
#addresses-container {
transition: opacity 0.3s ease;
}
</style>
<!-- Addresses content --> <!-- Addresses content -->
<div class="col-lg-9"> <div class="col-lg-9">
<div class="ps-lg-3 ps-xl-0"> <div class="ps-lg-3 ps-xl-0">
@ -8,137 +55,31 @@
<!-- Page title --> <!-- Page title -->
<h1 class="h2 mb-1 mb-sm-2">{{ __('addresses.page_title') }}</h1> <h1 class="h2 mb-1 mb-sm-2">{{ __('addresses.page_title') }}</h1>
@foreach ($addresses as $address) <!-- Loading indicator -->
<div class="border-bottom py-4"> <div id="addresses-loading" class="text-center py-4">
<div class="nav flex-nowrap align-items-center justify-content-between pb-1 mb-3"> <div class="shimmer-wrapper">
<div class="d-flex align-items-center gap-3 me-4"> <div class="shimmer">
<h2 class="h6 mb-0">{{ $address->label }}</h2> <div class="shimmer-line"></div>
@if ($address->is_primary) <div class="shimmer-line"></div>
<span class="badge text-bg-info rounded-pill">{{ __('addresses.primary') }}</span> <div class="shimmer-line short"></div>
@endif
</div>
<a class="nav-link hiding-collapse-toggle text-decoration-underline p-0 collapsed primaryAddressEditButton"
data-address-id="{{ $address->id }}" href=".primary-address-{{ $address->id }}"
data-bs-toggle="collapse" aria-expanded="false"
aria-controls="primaryAddressPreview{{ $address->id }} primaryAddressEdit{{ $address->id }}">{{ __('addresses.edit') }}</a>
</div> </div>
<div class="collapse primary-address-{{ $address->id }} show" <div class="shimmer">
id="primaryAddressPreview{{ $address->id }}"> <div class="shimmer-line"></div>
<ul class="list-unstyled fs-sm m-0"> <div class="shimmer-line"></div>
<li>{{ $address->location }}</li> <div class="shimmer-line short"></div>
<li>{{ $address->address }}</li>
</ul>
</div> </div>
<div class="collapse primary-address-{{ $address->id }}" id="primaryAddressEdit{{ $address->id }}"> <div class="shimmer">
<div class="shimmer-line"></div>
{{-- add message on success or error --}} <div class="shimmer-line"></div>
<div class="alert alert-danger d-none address-form-message" role="alert"></div> <div class="shimmer-line short"></div>
<div class="alert alert-success d-none address-form-success" role="alert"></div>
<form class="row g-3 g-sm-4 needs-validation address-form" novalidate
action="{{ route('addresses.update', $address->id) }}" method="POST">
@csrf
@method('PUT')
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.province') }}</label>
<select class="form-select province-select" required
data-address-id="{{ $address->id }}"
data-province-id="{{ $address->province_id }}"
data-city-id="{{ $address->city_id }}"
data-district-id="{{ $address->district_id }}"
data-village-id="{{ $address->subdistrict_id }}">
<option value="">{{ __('addresses.select_province') }}</option>
</select>
<div class="invalid-feedback">{{ __('addresses.please_select_province') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.city') }}</label>
<select class="form-select city-select" required data-address-id="{{ $address->id }}">
<option value="">{{ __('addresses.select_city') }}</option>
</select>
<div class="invalid-feedback">{{ __('addresses.please_select_city') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.district') }}</label>
<select class="form-select district-select" required
data-address-id="{{ $address->id }}">
<option value="">{{ __('addresses.select_district') }}</option>
</select>
<div class="invalid-feedback">{{ __('addresses.please_select_district') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.village') }}</label>
<select class="form-select village-select" required
data-address-id="{{ $address->id }}">
<option value="">{{ __('addresses.select_village') }}</option>
</select>
<div class="invalid-feedback">{{ __('addresses.please_select_village') }}</div>
</div>
</div>
<div class="col-sm-4">
<div class="position-relative">
<label for="psa-zip-{{ $address->id }}"
class="form-label">{{ __('addresses.zip_code') }}</label>
<input type="text" class="form-control" id="psa-zip-{{ $address->id }}"
value="{{ $address->postal_code }}" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_zip_code') }}</div>
</div>
</div>
<div class="col-sm-8">
<div class="position-relative">
<label for="psa-address-{{ $address->id }}"
class="form-label">{{ __('addresses.address') }}</label>
<input type="text" class="form-control" id="psa-address-{{ $address->id }}"
value="{{ $address->address }}" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_address') }}</div>
</div>
</div>
<div class="col-12">
<div class="form-check mb-0">
<input type="checkbox" class="form-check-input" id="set-primary-{{ $address->id }}"
{{ $address->is_primary ? 'checked' : '' }}>
<label for="set-primary-{{ $address->id }}"
class="form-check-label">{{ __('addresses.set_as_primary_address') }}</label>
</div>
</div>
<div class="col-12 d-flex justify-content-between">
<div class="d-flex gap-3 pt-2 pt-sm-0">
<button type="submit"
class="btn btn-primary">{{ __('addresses.save_changes') }}</button>
<button type="button" class="btn btn-secondary" data-bs-toggle="collapse"
data-bs-target=".primary-address-{{ $address->id }}" aria-expanded="true"
aria-controls="primaryAddressPreview{{ $address->id }} primaryAddressEdit{{ $address->id }}">{{ __('addresses.close') }}</button>
</div>
<button type="button" class="btn btn-danger delete-address-btn"
data-address-id="{{ $address->id }}"
data-address-label="{{ $address->label }}">{{ __('addresses.delete') }}</button>
</div>
</form>
</div> </div>
</div> </div>
@endforeach <p class="text-muted mt-3">{{ __('addresses.loading') }}</p>
</div>
<!-- Addresses container -->
<div id="addresses-container">
<!-- Add address button --> <!-- Addresses will be loaded here via AJAX -->
<div class="nav pt-4">
<a class="nav-link animate-underline fs-base px-0" href="#newAddressModal" data-bs-toggle="modal">
<i class="ci-plus fs-lg ms-n1 me-2"></i>
<span class="animate-target">{{ __('addresses.add_address') }}</span>
</a>
</div> </div>
</div> </div>
</div> </div>
@ -236,6 +177,26 @@
</div> </div>
</div> </div>
<!-- Notification Modal -->
<div class="modal fade" id="notificationModal" tabindex="-1" aria-labelledby="notificationModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="notificationModalLabel">{{ __('addresses.notification') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="d-flex align-items-center">
<div class="notification-icon me-3">
<div class="spinner-border text-primary" role="status"></div>
</div>
<div class="notification-message" id="notificationMessage"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal --> <!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteAddressModal" tabindex="-1" aria-labelledby="deleteAddressModalLabel" <div class="modal fade" id="deleteAddressModal" tabindex="-1" aria-labelledby="deleteAddressModalLabel"
aria-hidden="true"> aria-hidden="true">
@ -270,6 +231,218 @@
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
/* ===============================
* LOAD ADDRESSES VIA AJAX
* =============================== */
function loadAddresses() {
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const loadingElement = document.getElementById('addresses-loading');
const containerElement = document.getElementById('addresses-container');
// Show loading with shimmer
if (loadingElement) {
loadingElement.style.opacity = '1';
loadingElement.style.display = 'block';
}
if (containerElement) {
containerElement.style.opacity = '0';
containerElement.style.display = 'none';
}
fetch('/addresses', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': token,
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success && data.addresses) {
renderAddresses(data.addresses);
} else {
console.error('Failed to load addresses:', data);
showToast('Failed to load addresses', 'error');
}
})
.catch(error => {
console.error('Error loading addresses:', error);
showToast('Error loading addresses', 'error');
})
.finally(() => {
// Smooth transition from loading to content
setTimeout(() => {
if (loadingElement) {
loadingElement.style.opacity = '0';
setTimeout(() => {
loadingElement.style.display = 'none';
}, 300);
}
if (containerElement) {
containerElement.style.opacity = '1';
containerElement.style.display = 'block';
}
}, 100);
});
}
function reloadAddresses() {
loadAddresses();
}
function renderAddresses(addresses) {
const container = document.getElementById('addresses-container');
if (!container) return;
let html = '';
addresses.forEach(address => {
html += `
<div class="border-bottom py-4">
<div class="nav flex-nowrap align-items-center justify-content-between pb-1 mb-3">
<div class="d-flex align-items-center gap-3 me-4">
<h2 class="h6 mb-0">${address.label}</h2>
${address.is_primary ? '<span class="badge text-bg-info rounded-pill">{{ __("addresses.primary") }}</span>' : ''}
</div>
<a class="nav-link hiding-collapse-toggle text-decoration-underline p-0 collapsed primaryAddressEditButton"
data-address-id="${address.id}" href=".primary-address-${address.id}"
data-bs-toggle="collapse" aria-expanded="false"
aria-controls="primaryAddressPreview${address.id} primaryAddressEdit${address.id}">{{ __('addresses.edit') }}</a>
</div>
<div class="collapse primary-address-${address.id} show"
id="primaryAddressPreview${address.id}">
<ul class="list-unstyled fs-sm m-0">
<li>${address.location}</li>
<li>${address.address}</li>
</ul>
</div>
<div class="collapse primary-address-${address.id}" id="primaryAddressEdit${address.id}">
<div class="alert alert-danger d-none address-form-message" role="alert"></div>
<div class="alert alert-success d-none address-form-success" role="alert"></div>
<form class="row g-3 g-sm-4 needs-validation address-form" novalidate
action="/addresses/${address.id}" method="POST">
<input type="hidden" name="_method" value="PUT">
<input type="hidden" name="_token" value="${document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''}">
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.province') }}</label>
<select class="form-select province-select" required
data-address-id="${address.id}"
data-province-id="${address.province_id}"
data-city-id="${address.city_id}"
data-district-id="${address.district_id}"
data-village-id="${address.subdistrict_id}">
<option value="">{{ __('addresses.select_province') }}</option>
</select>
<div class="invalid-feedback">{{ __('addresses.please_select_province') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.city') }}</label>
<select class="form-select city-select" required data-address-id="${address.id}">
<option value="">{{ __('addresses.select_city') }}</option>
</select>
<div class="invalid-feedback">{{ __('addresses.please_select_city') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.district') }}</label>
<select class="form-select district-select" required data-address-id="${address.id}">
<option value="">{{ __('addresses.select_district') }}</option>
</select>
<div class="invalid-feedback">{{ __('addresses.please_select_district') }}</div>
</div>
</div>
<div class="col-sm-6">
<div class="position-relative">
<label class="form-label">{{ __('addresses.village') }}</label>
<select class="form-select village-select" required data-address-id="${address.id}">
<option value="">{{ __('addresses.select_village') }}</option>
</select>
<div class="invalid-feedback">{{ __('addresses.please_select_village') }}</div>
</div>
</div>
<div class="col-sm-4">
<div class="position-relative">
<label class="form-label">{{ __('addresses.zip_code') }}</label>
<input type="text" class="form-control postal_code" value="${address.postal_code}" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_zip_code') }}</div>
</div>
</div>
<div class="col-sm-8">
<div class="position-relative">
<label class="form-label">{{ __('addresses.address') }}</label>
<input type="text" class="form-control zip_code" value="${address.address}" required>
<div class="invalid-feedback">{{ __('addresses.please_enter_address') }}</div>
</div>
</div>
<div class="col-12">
<div class="form-check mb-0">
<input type="checkbox" class="form-check-input" ${address.is_primary ? 'checked' : ''}>
<label class="form-check-label">{{ __('addresses.set_as_primary_address') }}</label>
</div>
</div>
<div class="col-12 d-flex justify-content-between">
<div class="d-flex gap-3 pt-2 pt-sm-0">
<button type="submit" class="btn btn-primary">{{ __('addresses.save_changes') }}</button>
<button type="button" class="btn btn-secondary" data-bs-toggle="collapse"
data-bs-target=".primary-address-${address.id}" aria-expanded="true"
aria-controls="primaryAddressPreview${address.id} primaryAddressEdit${address.id}">{{ __('addresses.close') }}</button>
</div>
<button type="button" class="btn btn-danger delete-address-btn"
data-address-id="${address.id}"
data-address-label="${address.label}">{{ __('addresses.delete') }}</button>
</div>
</form>
</div>
</div>
`;
});
// Add address button
html += `
<div class="nav pt-4">
<a class="nav-link animate-underline fs-base px-0" href="#newAddressModal" data-bs-toggle="modal">
<i class="ci-plus fs-lg ms-n1 me-2"></i>
<span class="animate-target">{{ __('addresses.add_address') }}</span>
</a>
</div>
`;
container.innerHTML = html;
// Initialize address forms after rendering
initializeAddressForms();
}
function initializeAddressForms() {
// Initialize province selects for each address
document.querySelectorAll('.province-select').forEach(select => {
const addressId = select.dataset.addressId;
const provinceId = select.dataset.provinceId;
const cityId = select.dataset.cityId;
const districtId = select.dataset.districtId;
const villageId = select.dataset.villageId;
// Load provinces and set initial values
loadProvinces(select, addressId, provinceId, cityId, districtId, villageId);
});
}
// Load addresses on page load
loadAddresses();
/* =============================== /* ===============================
* INIT EDIT BUTTON * INIT EDIT BUTTON
* =============================== */ * =============================== */
@ -721,19 +894,19 @@
console.log(result); console.log(result);
if (result.success) { if (result.success) {
// Show success message and reload page // Show success message and reload page
showToast(result.message, 'success'); showNotification(result.message, 'success');
setTimeout(() => { setTimeout(() => {
window.location.reload(); reloadAddresses();
}, 1000); }, 1000);
} else { } else {
// Show error message // Show error message
showToast(result.message || '{{ __('addresses.error_saving_address') }}', showNotification(result.message || '{{ __('addresses.error_saving_address') }}',
'error'); 'error');
} }
}) })
.catch(error => { .catch(error => {
console.error('Error:', error); console.error('Error:', error);
showToast('{{ __('addresses.error_saving_address') }}', 'error'); showNotification('{{ __('addresses.error_saving_address') }}', 'error');
}) })
.finally(() => { .finally(() => {
deleteAddressId = null; deleteAddressId = null;
@ -764,30 +937,46 @@
.addressId; .addressId;
// Prepare form data for AJAX submission // Prepare form data for AJAX submission
const submitData = { const submitData = {};
_token: formData.get('_token'),
province_id: isNewAddress ? // Get CSRF token
form.querySelector('.new-province-select').value : form.querySelector( const tokenInput = form.querySelector('input[name="_token"]');
'.province-select').value, if (tokenInput) submitData._token = tokenInput.value;
city_id: isNewAddress ?
form.querySelector('.new-city-select').value : form.querySelector( // Get province ID
'.city-select').value, const provinceSelect = isNewAddress ?
district_id: isNewAddress ? form.querySelector('.new-province-select') : form.querySelector('.province-select');
form.querySelector('.new-district-select').value : form.querySelector( if (provinceSelect) submitData.province_id = provinceSelect.value;
'.district-select').value,
subdistrict_id: isNewAddress ? // Get city ID
form.querySelector('.new-village-select').value : form.querySelector( const citySelect = isNewAddress ?
'.village-select').value, form.querySelector('.new-city-select') : form.querySelector('.city-select');
postal_code: isNewAddress ? if (citySelect) submitData.city_id = citySelect.value;
form.querySelector('#new-zip').value : form.querySelector(
'input[id^="psa-zip-"]').value, // Get district ID
address: isNewAddress ? const districtSelect = isNewAddress ?
form.querySelector('#new-address').value : form.querySelector( form.querySelector('.new-district-select') : form.querySelector('.district-select');
'input[id^="psa-address-"]').value, if (districtSelect) submitData.district_id = districtSelect.value;
is_primary: (isNewAddress ?
form.querySelector('#set-primary-new') : // Get subdistrict ID
form.querySelector('input[id^="set-primary-"]')).checked ? '1' : '0' const villageSelect = isNewAddress ?
}; form.querySelector('.new-village-select') : form.querySelector('.village-select');
if (villageSelect) submitData.subdistrict_id = villageSelect.value;
// Get postal code
const zipInput = isNewAddress ?
form.querySelector('#new-zip') : form.querySelector('.postal_code');
if (zipInput) submitData.postal_code = zipInput.value;
// Get address
const addressInput = isNewAddress ?
form.querySelector('#new-address') : form.querySelector('.zip_code');
if (addressInput) submitData.address = addressInput.value;
// Get primary checkbox
const primaryCheckbox = isNewAddress ?
form.querySelector('#set-primary-new') : form.querySelector('input[id^="set-primary-"]');
if (primaryCheckbox) submitData.is_primary = primaryCheckbox.checked ? '1' : '0';
if (!isNewAddress) { if (!isNewAddress) {
submitData._method = 'PUT'; submitData._method = 'PUT';
@ -824,7 +1013,7 @@
setTimeout(() => { setTimeout(() => {
const modal = bootstrap.Modal.getInstance(newAddressModal); const modal = bootstrap.Modal.getInstance(newAddressModal);
if (modal) modal.hide(); if (modal) modal.hide();
window.location.reload(); reloadAddresses();
}, 1500); }, 1500);
} else { } else {
// Close the edit form and show preview after delay // Close the edit form and show preview after delay
@ -840,7 +1029,7 @@
} }
// Reload page to show updated data // Reload page to show updated data
window.location.reload(); reloadAddresses();
}, 2000); }, 2000);
} }
} else { } else {
@ -886,13 +1075,15 @@
// Toast notification helper // Toast notification helper
function showToast(message, type = 'info') { function showToast(message, type = 'info') {
// Create toast element if it doesn't exist console.log('Showing toast:', message, type);
// Create toast container if it doesn't exist
let toastContainer = document.getElementById('toast-container'); let toastContainer = document.getElementById('toast-container');
if (!toastContainer) { if (!toastContainer) {
toastContainer = document.createElement('div'); toastContainer = document.createElement('div');
toastContainer.id = 'toast-container'; toastContainer.id = 'toast-container';
toastContainer.className = 'position-fixed top-0 end-0 p-3'; toastContainer.className = 'position-fixed top-0 end-0 p-3';
toastContainer.style.zIndex = '1050'; toastContainer.style.zIndex = '1055';
document.body.appendChild(toastContainer); document.body.appendChild(toastContainer);
} }
@ -903,25 +1094,52 @@
<div class="toast-body"> <div class="toast-body">
${message} ${message}
</div> </div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button> <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div> </div>
</div> </div>
`; `;
toastContainer.insertAdjacentHTML('beforeend', toastHtml); toastContainer.insertAdjacentHTML('beforeend', toastHtml);
// Show toast using Bootstrap if available, otherwise fallback to simple alert
const toastElement = document.getElementById(toastId); const toastElement = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastElement, { if (typeof bootstrap !== 'undefined' && bootstrap.Toast) {
autohide: true, try {
delay: 3000 const toast = new bootstrap.Toast(toastElement, {
}); autohide: true,
delay: 3000
});
toast.show();
toast.show(); // Remove toast element after hidden
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
} catch (error) {
console.error('Bootstrap Toast error:', error);
// Fallback to simple notification
toastElement.style.display = 'block';
toastElement.style.position = 'fixed';
toastElement.style.top = '20px';
toastElement.style.right = '20px';
toastElement.style.zIndex = '1055';
// Remove toast element after hidden setTimeout(() => {
toastElement.addEventListener('hidden.bs.toast', () => { toastElement.style.display = 'none';
toastElement.remove(); toastElement.remove();
}); }, 3000);
}
} else {
// Fallback for when Bootstrap is not available
console.warn('Bootstrap Toast not available, using fallback');
alert(message);
}
}
// Toast notification helper (deprecated - use showNotification instead)
function showToast(message, type = 'info') {
console.warn('showToast is deprecated, use showNotification instead');
showNotification(message, type);
} }
/* =============================== /* ===============================