initial: bootstrap from BukidBountyApp base
This commit is contained in:
227
resources/js/composables/Core/useNavigate.js
Normal file
227
resources/js/composables/Core/useNavigate.js
Normal file
@@ -0,0 +1,227 @@
|
||||
// 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 })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user