// resources/js/composables/Core/useNavigate.js import { getCurrentInstance, h } from 'vue' import { encodeHash, encodePayload } from '../useUrlEncoder' import { useUIStore } from '../../stores/ui' import { useModal } from './useModal' // Page module map — populated once from app.js for prefetching let _pageModules = null; export function setPageModules(modules) { _pageModules = modules; } // Track which pages have already been prefetched to avoid duplicate calls const _prefetchedPages = new Set(); export function useNavigate({ currentPage, currentProps, loading } = {}) { const instance = getCurrentInstance() const uiStore = useUIStore() const modal = useModal() // Try to get the global navigate helper first from window.$navigateHelper, // then fall back to instance?.proxy?.$navigate const globalNavigate = (window.$navigateHelper || instance?.proxy?.$navigate) /** * Prefetch a page component chunk by name. * Call on hover/focus to start loading the JS bundle before navigation. */ const prefetch = (pageName) => { if (!pageName || !_pageModules || _prefetchedPages.has(pageName)) return; const loader = _pageModules[pageName]; if (loader) { _prefetchedPages.add(pageName); loader().catch(() => { /* chunk may not exist, ignore */ }); } } const navigate = async (arg1, arg2 = {}) => { // Universal navigate handler that accepts: // 1. navigate({ page: 'Home', props: {} }) // 2. navigate('Home', { target: '...' }) let page, props, delay, fromPopState; if (typeof arg1 === 'string') { page = arg1; props = arg2.props || arg2 || {}; delay = arg2.delay || 0; fromPopState = arg2.fromPopState || false; } else { ({ page, props = {}, delay = 0, fromPopState = false } = arg1); } if (!page) { console.error('[useNavigate] Navigation failed: No page specified.', { arg1, arg2 }); return; } // Global Page Disabled Check — fire-and-forget refresh if stale (don't block navigation) const cacheThreshold = 30 * 1000; // 30 seconds const isStale = !uiStore.lastSynced || (new Date() - new Date(uiStore.lastSynced) > cacheThreshold); if (isStale) { // Non-blocking: refresh in background, don't await uiStore.refreshSettings().catch(e => { console.error('[useNavigate] Failed to refresh settings during navigation:', e); }); } const pageLower = page.toLowerCase(); const isDisabled = uiStore.disabledPages && uiStore.disabledPages.some(p => p.toLowerCase() === pageLower); if (isDisabled) { console.warn(`[useNavigate] Page ${page} is globally disabled.`); modal.open({ title: 'Access Restricted', body: h('div', { class: 'text-center p-4' }, [ h('div', { class: 'icon-shape bg-soft-danger text-danger rounded-circle mx-auto mb-4 p-3 shadow-sm' }, [ h('i', { class: 'fas fa-lock fa-3x' }) ]), h('h4', { class: 'fw-black mb-3 text-dark' }, 'Page Offline'), h('p', { class: 'text-muted mb-4 px-3' }, `The requested page (${page}) has been temporarily disabled by the system administrator.`), h('button', { class: 'btn btn-primary rounded-pill px-5 py-2 fw-bold shadow-primary-sm', onClick: () => { modal.close(); if (pageLower !== 'home') navigate({ page: 'Home' }); } }, 'Back to Dashboard') ]), footer: null }); if (loading) loading.value = false; return; } // If local refs are provided (root app usage), update them directly if (currentPage || currentProps || loading) { if (loading) loading.value = true if (delay) await new Promise(r => setTimeout(r, delay)) if (currentPage) currentPage.value = page if (currentProps) currentProps.value = props // Update browser URL (only if NOT coming from a popstate event) if (!fromPopState) { let url; if (page === 'Home') { url = '/'; } else if (page === 'Auth.Login') { url = '/login'; } else { // Check for special props that should be encoded into URL const hashkeyProp = props?.target || props?.target_user || props?.hashkey || props?.id; const payloadProp = props?.payload; let basePageUrl = `/${page .replace(/\./g, '/') .replace(/([a-z0-9])([A-Z])/g, '$1-$2') .toLowerCase()}`; if (hashkeyProp) { // Encode the hashkey into URL as --h:ENCODED const encodedHash = encodeHash(hashkeyProp); url = `${basePageUrl}--${encodedHash}`; } else if (payloadProp) { // Encode the payload into URL as --e:ENCODED_PAYLOAD const encodedPayload = encodePayload(payloadProp); url = `${basePageUrl}--${encodedPayload}`; } else { url = basePageUrl; } } window.history.pushState({ page, props }, '', url); } if (loading) loading.value = false return } // In child components, fall back to the global $navigate helper if (globalNavigate) { globalNavigate({ page, props, delay, fromPopState }) } else { console.warn('[useNavigate] No local refs and global navigate helper not found.') } } const reloadPage = () => { if (currentPage) { navigate({ page: currentPage.value, props: currentProps?.value, delay: 0 }) } else if (globalNavigate) { // In children, we can't easily get 'currentPage' unless we pass it or use a store // but usually reloadPage is used in the context that knows the current state. console.warn('[useNavigate] reloadPage called in child without refs.') } } return { navigate, reloadPage, prefetch } } /** * Helper to derive component name from a URL path in a generic way. * Matches the logic in VueRouteMap.php. */ const getPageFromUrl = (path) => { let cleanPath = path.trim().replace(/^\/+|\/+$/g, ''); if (!cleanPath) return 'Home'; // Handle explicit common mappings if (cleanPath === 'login') return 'Auth.Login'; if (cleanPath === 'app') return 'Home'; // Strip hash/payload suffix (e.g., "view-store-market--h:ABC" -> "view-store-market") const slug = cleanPath.split('--h:')[0].split('--e:')[0]; // Convert kebab-case/path to PascalCase/dots (e.g., "market/list-products" -> "Market.ListProducts") return slug.split('/').map(part => { return part.split('-').map(subPart => { if (!subPart) return ''; return subPart.charAt(0).toUpperCase() + subPart.slice(1); }).join(''); }).join('.'); } // Global popstate listener to handle browser back button if (typeof window !== 'undefined') { window.addEventListener('popstate', (event) => { if (event.state && event.state.page) { if (window.$navigateHelper) { window.$navigateHelper({ page: event.state.page, props: event.state.props, fromPopState: true, delay: 0 }) } else { // Fallback if app not yet initialized window.location.reload() } } else { // If no state (initial load entry point), attempt to restore from initial DOM payload const el = document.getElementById('main-body'); if (el && el.dataset.page) { try { const initialPage = JSON.parse(el.dataset.page); if (window.$navigateHelper) { window.$navigateHelper({ page: initialPage.component, props: initialPage.props, fromPopState: true, delay: 0 }); return; } } catch (e) { console.error('[useNavigate] Failed to parse initial page from DOM:', e); } } // Ultimate Fallback: Derive the page from the URL (without props, as we can't reliably decode them here) const page = getPageFromUrl(window.location.pathname); if (window.$navigateHelper) { window.$navigateHelper({ page, fromPopState: true, delay: 0 }) } } }) }