diff --git a/dev/devPage.tsx b/dev/devPage.tsx
index fb0a76d4..2943afbd 100644
--- a/dev/devPage.tsx
+++ b/dev/devPage.tsx
@@ -6,7 +6,7 @@ import { createRoot } from 'react-dom/client'
import { I18nextProvider, useTranslation } from 'react-i18next'
import 'tachyons'
import i18n from '../src/i18n.js'
-import { ExplorePage, StartExploringPage, IpldExploreForm, IpldCarExploreForm, ExploreProvider, HeliaProvider } from '../src/index.js'
+import { ExplorePage, StartExploringPage, IpldExploreForm, IpldCarExploreForm, ExploreProvider, HeliaProvider, useExplore } from '../src/index.js'
globalThis.Buffer = globalThis.Buffer ?? Buffer
@@ -64,27 +64,22 @@ const HeaderComponent: React.FC = () => {
}
const PageRenderer = (): React.ReactElement => {
- const [route, setRoute] = useState(window.location.hash.slice(1) ?? '/')
+ const { setExplorePath, exploreState: { path } } = useExplore()
useEffect(() => {
- const onHashChange = (): void => { setRoute(window.location.hash.slice(1) ?? '/') }
+ const onHashChange = (): void => {
+ const newRoute = window.location.hash ?? null
+ setExplorePath(newRoute)
+ }
window.addEventListener('hashchange', onHashChange)
return () => { window.removeEventListener('hashchange', onHashChange) }
- }, [])
+ }, [setExplorePath])
- const RenderPage: React.FC = () => {
- switch (true) {
- case route.startsWith('/explore'):
- return
- case route === '/':
- default:
- return
- }
+ if (path == null || path === '') {
+ return
}
- return (
-
- )
+ return
}
const App = (): React.ReactElement => {
diff --git a/src/components/ExplorePage.stories.tsx b/src/components/ExplorePage.stories.tsx
index 38ea832a..3cdf18eb 100644
--- a/src/components/ExplorePage.stories.tsx
+++ b/src/components/ExplorePage.stories.tsx
@@ -8,7 +8,6 @@ const defaultState: ExploreState = {
path: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
canonicalPath: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
error: null,
- explorePathFromHash: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
targetNode: {
type: 'dag-pb',
format: 'unixfs',
diff --git a/src/components/ExplorePage.tsx b/src/components/ExplorePage.tsx
index 09bc5b75..1ad887f4 100644
--- a/src/components/ExplorePage.tsx
+++ b/src/components/ExplorePage.tsx
@@ -26,9 +26,9 @@ export const ExplorePage = ({
const { t, ready: tReady } = useTranslation('explore')
const { exploreState, doExploreLink } = useExplore()
- const { explorePathFromHash } = exploreState
+ const { path } = exploreState
- if (explorePathFromHash == null) {
+ if (path == null) {
// No IPLD path to explore so show the intro page
console.warn('[IPLD Explorer] ExplorePage loaded without a path to explore')
return null
diff --git a/src/components/loader/loader.tsx b/src/components/loader/loader.tsx
index 4edbee5d..f19226f6 100644
--- a/src/components/loader/loader.tsx
+++ b/src/components/loader/loader.tsx
@@ -1,7 +1,7 @@
import React from 'react'
import styles from './loader.module.css'
-export const Loader: React.FC<{ color: string }> = ({ color = 'light', ...props }) => {
+export const Loader: React.FC<{ color?: string }> = ({ color = 'light', ...props }) => {
const className = `dib ${styles.laBallTrianglePath} la-${color} la-sm`
return (
diff --git a/src/providers/explore.tsx b/src/providers/explore.tsx
index 67d1fe60..343a1574 100644
--- a/src/providers/explore.tsx
+++ b/src/providers/explore.tsx
@@ -12,7 +12,9 @@ import type { NormalizedDagNode } from '../types.js'
interface ExploreContextProps {
exploreState: ExploreState
- // explorePathFromHash: string | null
+ explorePathPrefix: string
+ isLoading: boolean
+ setExplorePath(path: string | null): void
doExploreLink(link: any): void
doExploreUserProvidedPath(path: string): void
doUploadUserProvidedCar(file: File, uploadImage: string): Promise
@@ -37,7 +39,6 @@ export interface ExploreState {
nodes: any[]
pathBoundaries: any[]
error: IpldExploreError | null
- explorePathFromHash: string | null
}
export const ExploreContext = createContext(undefined)
@@ -58,6 +59,24 @@ const getCidFromCidOrFqdn = (cidOrFqdn: CID | string): CID => {
return CID.parse(cidOrFqdn)
}
+const processPath = (path: string | null, pathPrefix: string): string | null => {
+ let newPath = path
+ if (newPath != null) {
+ if (newPath.includes(pathPrefix)) {
+ newPath = newPath.slice(pathPrefix.length)
+ }
+ if (newPath.startsWith('/')) {
+ newPath = newPath.slice(1)
+ }
+ if (newPath === '') {
+ newPath = null
+ } else {
+ newPath = decodeURIComponent(newPath)
+ }
+ }
+ return newPath
+}
+
const defaultState: ExploreState = {
path: null,
targetNode: null,
@@ -65,44 +84,83 @@ const defaultState: ExploreState = {
localPath: '',
nodes: [],
pathBoundaries: [],
- error: null,
- explorePathFromHash: null
+ error: null
+}
+
+export interface ExploreProviderProps {
+ children?: ReactNode | ReactNode[]
+ state?: Partial
+ explorePathPrefix?: string
}
-export const ExploreProvider = ({ children, state = defaultState }: { children?: ReactNode, state?: ExploreState }): React.ReactNode => {
- const [exploreState, setExploreState] = useState({ ...state, explorePathFromHash: window.location.hash.slice('#/explore'.length) })
+export const ExploreProvider = ({ children, state, explorePathPrefix = '#/explore' }: ExploreProviderProps): React.ReactNode => {
+ if (state == null) {
+ state = {
+ path: processPath(window.location.hash, explorePathPrefix)
+ }
+ } else {
+ if (state.path === '') {
+ state.path = null
+ } else if (state.path != null) {
+ state.path = processPath(state.path, explorePathPrefix)
+ }
+ }
+ const [exploreState, setExploreState] = useState({ ...defaultState, ...state })
const { helia } = useHelia()
- const { explorePathFromHash } = exploreState
+ const [isLoading, setIsLoading] = useState(false)
+ const { path } = exploreState
- const fetchExploreData = useCallback(async (path: string): Promise => {
- // Clear the target node when a new path is requested
- setExploreState((exploreState) => ({
- ...exploreState,
- targetNode: null
- }))
- const pathParts = parseIpldPath(path)
- if (pathParts == null || helia == null) return
+ useEffect(() => {
+ setIsLoading(true);
- const { cidOrFqdn, rest } = pathParts
- try {
- const cid = getCidFromCidOrFqdn(cidOrFqdn)
- const { targetNode, canonicalPath, localPath, nodes, pathBoundaries } = await resolveIpldPath(helia, cid, rest)
-
- setExploreState(({ explorePathFromHash }) => ({
- explorePathFromHash,
- path,
- targetNode,
- canonicalPath,
- localPath,
- nodes,
- pathBoundaries,
- error: null
+ (async () => {
+ if (path == null || helia == null) {
+ return
+ }
+ // Clear the target node when a new path is requested
+ setExploreState((exploreState) => ({
+ ...exploreState,
+ targetNode: null
}))
- } catch (error: any) {
- console.warn('Failed to resolve path', path, error)
- setExploreState((prevState) => ({ ...prevState, error }))
+ const pathParts = parseIpldPath(path)
+ if (pathParts == null || helia == null) return
+
+ const { cidOrFqdn, rest } = pathParts
+ try {
+ const cid = getCidFromCidOrFqdn(cidOrFqdn)
+ const { targetNode, canonicalPath, localPath, nodes, pathBoundaries } = await resolveIpldPath(helia, cid, rest)
+
+ setExploreState((curr) => ({
+ ...curr,
+ targetNode,
+ canonicalPath,
+ localPath,
+ nodes,
+ pathBoundaries,
+ error: null
+ }))
+ } catch (error: any) {
+ console.warn('Failed to resolve path', path, error)
+ setExploreState((prevState) => ({ ...prevState, error }))
+ }
+ })().catch((err) => {
+ console.error('Error fetching explore data', err)
+ setExploreState((prevState) => ({ ...prevState, error: err }))
+ }).finally(() => {
+ setIsLoading(false)
+ })
+ }, [helia, path])
+
+ const setExplorePath = (path: string | null): void => {
+ const newPath = processPath(path, explorePathPrefix)
+ if (newPath != null && !window.location.href.includes(newPath)) {
+ throw new Error('setExplorePath should only be used to update the state, not the URL. If you are using a routing library that doesn\'t allow you to listen to hashchange events, ensure the URL is updated prior to calling setExplorePath.')
}
- }, [helia])
+ setExploreState((exploreState) => ({
+ ...exploreState,
+ path: newPath
+ }))
+ }
const doExploreLink = (link: LinkObject): void => {
const { nodes, pathBoundaries } = exploreState
@@ -114,12 +172,16 @@ export const ExploreProvider = ({ children, state = defaultState }: { children?:
}
pathParts.unshift(cid)
const path = pathParts.map((part) => encodeURIComponent(part)).join('/')
- const hash = `#/explore/${path}`
+ const hash = `${explorePathPrefix}/${path}`
window.location.hash = hash
+ setExplorePath(path)
}
+ /**
+ * @deprecated - use setExplorePath instead
+ */
const doExploreUserProvidedPath = (path: string): void => {
- const hash = path != null ? `#/explore${ensureLeadingSlash(path)}` : '#/explore'
+ const hash = path != null ? `${explorePathPrefix}${ensureLeadingSlash(path)}` : explorePathPrefix
window.location.hash = hash
}
@@ -130,7 +192,7 @@ export const ExploreProvider = ({ children, state = defaultState }: { children?:
}
try {
const rootCid = await importCar(file, helia)
- const hash = rootCid.toString() != null ? `#/explore${ensureLeadingSlash(rootCid.toString())}` : '#/explore'
+ const hash = rootCid.toString() != null ? `${explorePathPrefix}${ensureLeadingSlash(rootCid.toString())}` : explorePathPrefix
window.location.hash = hash
const imageFileLoader = document.getElementById('car-loader-image') as HTMLImageElement
@@ -140,42 +202,14 @@ export const ExploreProvider = ({ children, state = defaultState }: { children?:
} catch (err) {
console.error('Could not import car file', err)
}
- }, [helia])
-
- useEffect(() => {
- const handleHashChange = (): void => {
- const explorePathFromHash = window.location.hash.slice('#/explore'.length)
-
- setExploreState((state) => ({
- ...state,
- explorePathFromHash
- }))
- }
-
- window.addEventListener('hashchange', handleHashChange)
- handleHashChange()
-
- return () => {
- window.removeEventListener('hashchange', handleHashChange)
- }
- }, [])
-
- useEffect(() => {
- // if explorePathFromHash or helia change and are not null, fetch the data
- // We need to check for helia because the helia provider is async and may not be ready yet
- if (explorePathFromHash != null && helia != null) {
- void (async () => {
- await fetchExploreData(decodeURIComponent(explorePathFromHash))
- })()
- }
- }, [helia, explorePathFromHash])
+ }, [explorePathPrefix, helia])
if (helia == null) {
return
}
return (
-
+
{children}
)