| 1 |
import { Dashboard } from '@assist/pages/Dashboard'; |
| 2 |
import { homeIcon } from '@assist/svg'; |
| 3 |
import { safeParseJson } from '@shared/lib/parsing'; |
| 4 |
import { useActivityStore } from '@shared/state/activity'; |
| 5 |
import apiFetch from '@wordpress/api-fetch'; |
| 6 |
import { useCallback, useEffect, useLayoutEffect } from '@wordpress/element'; |
| 7 |
import { __ } from '@wordpress/i18n'; |
| 8 |
import { create } from 'zustand'; |
| 9 |
import { createJSONStorage, devtools, persist } from 'zustand/middleware'; |
| 10 |
|
| 11 |
const pages = [ |
| 12 |
{ |
| 13 |
slug: 'dashboard', |
| 14 |
name: __('Dashboard', 'extendify-local'), |
| 15 |
icon: homeIcon, |
| 16 |
component: Dashboard, |
| 17 |
}, |
| 18 |
]; |
| 19 |
|
| 20 |
let onChangeEvents = []; |
| 21 |
const state = (set, get) => ({ |
| 22 |
history: [], |
| 23 |
viewedPages: [], |
| 24 |
current: null, |
| 25 |
// initialize the state with default values |
| 26 |
...(safeParseJson(window.extAssistData.userData.routerData)?.state ?? {}), |
| 27 |
setCurrent: async (page) => { |
| 28 |
if (!page) return; |
| 29 |
for (const event of onChangeEvents) { |
| 30 |
await event(page, { ...get() }); |
| 31 |
} |
| 32 |
// If history is the same, dont add (they pressed the same nav button) |
| 33 |
if (get().history[0]?.slug === page.slug) return; |
| 34 |
useActivityStore.getState().incrementActivity(`assist-${page.slug}`); |
| 35 |
set((state) => { |
| 36 |
const lastViewedAt = new Date().toISOString(); |
| 37 |
const firstViewedAt = lastViewedAt; |
| 38 |
const visited = state.viewedPages.find((a) => a.slug === page.slug); |
| 39 |
return { |
| 40 |
history: [page, ...state.history].filter(Boolean), |
| 41 |
current: page, |
| 42 |
viewedPages: [ |
| 43 |
// Remove the page if it's already in the list |
| 44 |
...state.viewedPages.filter((a) => a.slug !== page.slug), |
| 45 |
// Either add the page or update the count |
| 46 |
visited |
| 47 |
? { ...visited, count: visited.count + 1, lastViewedAt } |
| 48 |
: { |
| 49 |
slug: page.slug, |
| 50 |
firstViewedAt, |
| 51 |
lastViewedAt, |
| 52 |
count: 1, |
| 53 |
}, |
| 54 |
], |
| 55 |
}; |
| 56 |
}); |
| 57 |
}, |
| 58 |
}); |
| 59 |
|
| 60 |
const path = '/extendify/v1/assist/router-data'; |
| 61 |
const storage = { |
| 62 |
getItem: async () => await apiFetch({ path }), |
| 63 |
setItem: async (_name, state) => |
| 64 |
await apiFetch({ path, method: 'POST', data: { state } }), |
| 65 |
}; |
| 66 |
|
| 67 |
const useRouterState = create( |
| 68 |
persist(devtools(state, { name: 'Extendify Assist Router' }), { |
| 69 |
name: 'extendify-assist-router', |
| 70 |
storage: createJSONStorage(() => storage), |
| 71 |
skipHydration: true, |
| 72 |
partialize: ({ viewedPages }) => ({ viewedPages }), |
| 73 |
}), |
| 74 |
); |
| 75 |
export const router = { |
| 76 |
onRouteChange: (event) => { |
| 77 |
// dont add if duplicate |
| 78 |
if (onChangeEvents.includes(event)) return; |
| 79 |
onChangeEvents = [...onChangeEvents, event]; |
| 80 |
}, |
| 81 |
removeOnRouteChange: (event) => { |
| 82 |
onChangeEvents = onChangeEvents.filter((e) => e !== event); |
| 83 |
}, |
| 84 |
}; |
| 85 |
|
| 86 |
let once = false; |
| 87 |
export const useRouter = () => { |
| 88 |
const { current, setCurrent, history } = useRouterState(); |
| 89 |
const Component = current?.component ?? (() => null); |
| 90 |
|
| 91 |
const navigateTo = (slug) => { |
| 92 |
if (window.location.hash === `#${slug}`) { |
| 93 |
// Fire the event only |
| 94 |
window.dispatchEvent(new Event('hashchange')); |
| 95 |
return; |
| 96 |
} |
| 97 |
window.location.hash = `#${slug}`; |
| 98 |
}; |
| 99 |
useLayoutEffect(() => { |
| 100 |
// if no hash is present use previous or add #dashboard |
| 101 |
if (!window.location.hash) { |
| 102 |
window.location.hash = `#${current?.slug ?? 'dashboard'}`; |
| 103 |
} |
| 104 |
}, [current]); |
| 105 |
|
| 106 |
useEffect(() => { |
| 107 |
if (once) return; |
| 108 |
once = true; |
| 109 |
// watch url changes for #dashboard, etc |
| 110 |
const handle = () => { |
| 111 |
const hash = window.location.hash.replace('#', ''); |
| 112 |
const page = pages.find((page) => page.slug === hash); |
| 113 |
if (!page) { |
| 114 |
navigateTo(current?.slug ?? 'dashboard'); |
| 115 |
return; |
| 116 |
} |
| 117 |
setCurrent(page); |
| 118 |
// Update title to match the page |
| 119 |
document.title = page.name; |
| 120 |
}; |
| 121 |
window.addEventListener('hashchange', handle); |
| 122 |
if (!current) handle(); |
| 123 |
return () => { |
| 124 |
once = false; |
| 125 |
window.removeEventListener('hashchange', handle); |
| 126 |
}; |
| 127 |
}, [current, setCurrent]); |
| 128 |
|
| 129 |
return { |
| 130 |
current, |
| 131 |
CurrentPage: useCallback( |
| 132 |
() => ( |
| 133 |
<section aria-live="polite"> |
| 134 |
{/* Announce to SR on change */} |
| 135 |
<h1 className="sr-only">{current?.name}</h1> |
| 136 |
<Component /> |
| 137 |
</section> |
| 138 |
), |
| 139 |
[current], |
| 140 |
), |
| 141 |
pages, |
| 142 |
navigateTo, |
| 143 |
history, |
| 144 |
}; |
| 145 |
}; |
| 146 |
|