Skip to content

Commit

Permalink
fix: allow explore state to be updated (#457)
Browse files Browse the repository at this point in the history
  • Loading branch information
SgtPooki authored Oct 30, 2024
1 parent 20c36d5 commit 6299b86
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 85 deletions.
25 changes: 10 additions & 15 deletions dev/devPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 <ExplorePage />
case route === '/':
default:
return <StartExploringPage />
}
if (path == null || path === '') {
return <StartExploringPage />
}

return (
<RenderPage />
)
return <ExplorePage />
}

const App = (): React.ReactElement => {
Expand Down
1 change: 0 additions & 1 deletion src/components/ExplorePage.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const defaultState: ExploreState = {
path: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
canonicalPath: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
error: null,
explorePathFromHash: 'QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm',
targetNode: {
type: 'dag-pb',
format: 'unixfs',
Expand Down
4 changes: 2 additions & 2 deletions src/components/ExplorePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/components/loader/loader.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div {...props}>
Expand Down
166 changes: 100 additions & 66 deletions src/providers/explore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>
Expand All @@ -37,7 +39,6 @@ export interface ExploreState {
nodes: any[]
pathBoundaries: any[]
error: IpldExploreError | null
explorePathFromHash: string | null
}

export const ExploreContext = createContext<ExploreContextProps | undefined>(undefined)
Expand All @@ -58,51 +59,108 @@ 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,
canonicalPath: '',
localPath: '',
nodes: [],
pathBoundaries: [],
error: null,
explorePathFromHash: null
error: null
}

export interface ExploreProviderProps {
children?: ReactNode | ReactNode[]
state?: Partial<ExploreState>
explorePathPrefix?: string
}

export const ExploreProvider = ({ children, state = defaultState }: { children?: ReactNode, state?: ExploreState }): React.ReactNode => {
const [exploreState, setExploreState] = useState<ExploreState>({ ...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<ExploreState>({ ...defaultState, ...state })
const { helia } = useHelia()
const { explorePathFromHash } = exploreState
const [isLoading, setIsLoading] = useState<boolean>(false)
const { path } = exploreState

const fetchExploreData = useCallback(async (path: string): Promise<void> => {
// 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
Expand All @@ -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
}

Expand All @@ -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
Expand All @@ -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 <Loader color='dark' />
}

return (
<ExploreContext.Provider value={{ exploreState, doExploreLink, doExploreUserProvidedPath, doUploadUserProvidedCar }}>
<ExploreContext.Provider value={{ exploreState, explorePathPrefix, isLoading, doExploreLink, doExploreUserProvidedPath, doUploadUserProvidedCar, setExplorePath }} key={path}>
{children}
</ExploreContext.Provider>
)
Expand Down

0 comments on commit 6299b86

Please sign in to comment.