Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add helia/verified-fetch browser example #285

Merged
merged 25 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6b5e262
feat: add helia/verified-fetch browser example
2color Feb 6, 2024
72f3d59
feat(verified-fetch): improve UX (#286)
SgtPooki Feb 8, 2024
9da5dd5
Merge branch 'main' into add-verified-fetch
achingbrain Feb 8, 2024
981bcaa
Update examples/helia-browser-react-verified-fetch/package.json
2color Feb 8, 2024
1ace74a
refactor: clean up code and reduce deps
2color Feb 9, 2024
2ac6a75
chore: rename folder
2color Feb 9, 2024
1d7541c
chore: add test
2color Feb 9, 2024
6959b24
chore: fix linting
achingbrain Feb 9, 2024
32450e4
chore: add test to verified fetch example (#290)
achingbrain Feb 10, 2024
277d818
fix: improve rendering of cids on narrow viewports
2color Feb 12, 2024
17812d5
fix: improve auto content detection
2color Feb 12, 2024
11dafae
chore: fix tests
achingbrain Feb 13, 2024
a6790a4
chore: upgrade verified-fetch
2color Feb 29, 2024
c3f1069
feat: add link to the repo
2color Feb 29, 2024
c715238
chore: bump verified fetch version
2color Mar 14, 2024
26de16f
fix: add aborting functionality
2color Mar 19, 2024
e3924cd
Merge remote-tracking branch 'origin/main' into add-verified-fetch
2color Mar 25, 2024
4098ac3
fix: handle abort error
2color Mar 25, 2024
c4c2228
feat: add ability to fetch as dag-json
2color Mar 27, 2024
6d8c04f
chore: update deps
2color Mar 27, 2024
6da0ce2
chore: remove comment
2color Mar 27, 2024
1cbdf4b
chore: address feedback and simplify code
2color Mar 28, 2024
6148088
chore: reduce code duplication and add docs
2color Mar 28, 2024
d56f4d7
feat: add links to api docs and source
2color Mar 28, 2024
6c0116c
fix: linting error
2color Mar 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
matrix:
project:
- helia-101
- helia-browser-verified-fetch
- helia-cjs
- helia-electron
- helia-esbuild
Expand Down Expand Up @@ -86,6 +87,7 @@ jobs:
project:
- helia-101
- helia-cjs
- helia-browser-verified-fetch
- helia-electron
- helia-esbuild
- helia-jest
Expand Down
24 changes: 24 additions & 0 deletions examples/helia-browser-verified-fetch/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
34 changes: 34 additions & 0 deletions examples/helia-browser-verified-fetch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<p align="center">
<a href="https://github.com/ipfs/helia" title="Helia">
<img src="https://raw.githubusercontent.com/ipfs/helia/main/assets/helia.png" alt="Helia logo" width="300" />
</a>
</p>

<h3 align="center"><b>Browser Verified Retrieval with @helia/verified-fetch</b></h3>

<p align="center">
<img src="https://raw.githubusercontent.com/jlord/forkngo/gh-pages/badges/cobalt.png" width="200">
<br>
<a href="https://helia.io/modules/helia.html">Explore the docs</a>
·
<!-- <a href="https://codesandbox.io/">View Demo</a> -->
·
<a href="https://github.com/ipfs-examples/helia-examples/issues">Report Bug</a>
·
<a href="https://github.com/ipfs-examples/helia-examples/issues">Request Feature/Example</a>
</p>

<!-- omit from toc -->
## Table of Contents

- [Getting Started](#getting-started)
- [Installation and Running example](#installation-and-running-example)

## Getting Started

### Installation and Running example

```console
npm install
npm run dev
```
13 changes: 13 additions & 0 deletions examples/helia-browser-verified-fetch/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script>
<title>Verified Retrieval with @helia/verified-fetch</title>
</head>
<body>
<div class="" id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
28 changes: 28 additions & 0 deletions examples/helia-browser-verified-fetch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "helia-browser-verified-fetch",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"clean": "rimraf ./dist",
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vite build -c ./test/vite.config.js && test-browser-example test"
},
"dependencies": {
"@helia/verified-fetch": "next",
"@sgtpooki/file-type": "^1.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@playwright/test": "^1.31.2",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react": "^4.2.1",
"typescript": "^5.2.2",
"test-ipfs-example": "^1.0.0",
"vite": "^5.0.11"
}
}
236 changes: 236 additions & 0 deletions examples/helia-browser-verified-fetch/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import { verifiedFetch } from '@helia/verified-fetch'
import { fileTypeFromBuffer } from '@sgtpooki/file-type'
import { useCallback, useState } from 'react'
import { helpText } from './constants'

function renderOutput(output: string | JSX.Element, err: string): JSX.Element {
if (err.length > 0) {
return (
<div className="bg-red-300">
<pre className="bg-black text-red-300 rounded p-4 whitespace-pre-wrap break-words">{err}</pre>
</div>
)
}

if (typeof output === 'string') {
return (
<div className="bg-violet-300">
{output.length > 0 && (
<pre className="bg-black text-teal-300 rounded p-4 whitespace-pre-wrap break-words">
<code id="output" className="language-json">{`${output}`}</code>
</pre>
)}
</div>
)
}

return output
}

function loadingIndicator(message: string): JSX.Element {
return (
<div className="bg-yellow-300">
<pre className="bg-black text-yellow-300 rounded p-4">Loading... {message}</pre>
</div>
)
}

function App(): JSX.Element {
const [path, setPath] = useState<string>('')
const [output, setOutput] = useState<string | JSX.Element>('')
const [err, setErr] = useState<string>('')
const [loading, setLoadingTo] = useState<JSX.Element | null>(null)

const setSuccess = useCallback((message: string | JSX.Element) => {
setOutput(message)
setLoadingTo(null)
setErr('')
}, [])
const setError = useCallback((message: string) => {
setOutput('')
setLoadingTo(null)
setErr(message)
}, [])
const setLoading = useCallback((message: string) => {
setErr('')
setLoadingTo(loadingIndicator(message))
}, [])

const handleImageType = useCallback(async (resp: Response) => {
try {
setLoading('Waiting for full image data...')
const blob = await resp.blob()
const url = URL.createObjectURL(blob)
setSuccess(<img src={url} alt="fetched image content" />)
} catch (err) {
setError((err as Error).message)
}
}, [])

const handleJsonType = useCallback(async (resp: Response) => {
try {
setLoading('Waiting for full JSON data...')
const json = await resp.json()
setSuccess(JSON.stringify(json, null, 2))
} catch (err) {
setError((err as Error).message)
}
}, [])

const handleVideoType = useCallback(async (resp: Response) => {
try {
setLoading('Waiting for full video data...')
const blob = await resp.blob()
const url = URL.createObjectURL(blob)
setSuccess(<video controls src={url} />)
} catch (err) {
setError((err as Error).message)
}
}, [])

const onFetchJson = useCallback(async () => {
try {
setLoading('Fetching json response...')
const resp = await verifiedFetch(path)
await handleJsonType(resp)
} catch (err) {
setError((err as Error).message)
}
}, [path, handleJsonType])

const onFetchImage = useCallback(async () => {
try {
setLoading('Fetching image response...')
const resp = await verifiedFetch(path)
await handleImageType(resp)
} catch (err) {
setError((err as Error).message)
}
}, [path, handleImageType])

const onFetchFile = useCallback(async () => {
try {
setLoading('Fetching content to download...')
const resp = await verifiedFetch(path)
const blob = await resp.blob()
const url = URL.createObjectURL(blob)
const downloadLink = document.createElement('a')
downloadLink.href = url
downloadLink.download = 'download'
setSuccess('') // clear output
downloadLink.click()
} catch (err) {
setError((err as Error).message)
}
}, [path])

const onFetchAuto = useCallback(async () => {
if (path == null) {
setError('Invalid path')
return
}
try {
setLoading('Fetching auto content...')
const resp = await verifiedFetch(path)
const buffer = await resp.clone().arrayBuffer()
let contentType = (await fileTypeFromBuffer(new Uint8Array(buffer)))?.mime
if (!contentType) {
try {
// see if we can parse as json
await resp.clone().json()
contentType = 'application/json'
} catch (err) {
// ignore
}
}
switch (true) {
case contentType?.includes('image'):
await handleImageType(resp)
break
case contentType?.includes('json'):
await handleJsonType(resp)
break
case contentType?.includes('video'):
await handleVideoType(resp)
break
default:
setError(`Unknown content-type: ${contentType}`)
}
} catch (err) {
setError((err as Error).message)
}
}, [path, handleImageType, handleJsonType, handleVideoType])

return (
<div className="">
<section>
<div className="grid h-screen grid-cols-2">
{/* Left */}
<div className="bg-teal-200 p-4">
<div className="flex items-center space-x-4">
<a className="" href="https://github.com/ipfs/helia">
<img
className="h-20"
alt="Helia logo"
src="https://unpkg.com/@helia/css@1.0.1/logos/helia-logo.svg"
/>
</a>
<h1 className="text-2xl">
Verified Retrieval with <strong>@helia/verified-fetch</strong>
</h1>
</div>
<label className="block mt-4 mb-2 font-medium text-gray-900">
IPFS path to fetch
</label>
<input
type="text"
id="ipfs-path"
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="ipfs://... or ipns://"
onChange={(e) => {
setPath(e.target.value)
}}
value={path}
/>
<button
className="my-2 mr-2 btn btn-blue bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
id="button-fetch-json"
onClick={onFetchJson}
>
🔑 Fetch as JSON
</button>
<button
className="my-2 mr-2 btn btn-blue bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
id="button-fetch-image"
onClick={onFetchImage}
>
🔑 Fetch as image
</button>
<button
className="my-2 mr-2 btn btn-blue bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
id="button-fetch-file"
onClick={onFetchFile}
>
🔑 Fetch & Download
</button>
<button
className="my-2 mr-2 btn btn-blue bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
id="button-fetch-auto"
onClick={onFetchAuto}
>
🔑 Fetch auto
</button>

<pre className="bg-black text-teal-300 rounded p-4 whitespace-pre-wrap break-words">{helpText}</pre>
</div>
{/* Left */}
2color marked this conversation as resolved.
Show resolved Hide resolved

{/* Right */}
{renderOutput(loading ?? output, err)}
</div>
</section>
</div>
)
}

export default App
26 changes: 26 additions & 0 deletions examples/helia-browser-verified-fetch/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const helpText = `Example Paths
=========
IPNS name 👇
ipns://k51qzi5uqu5dhp48cti0590jyvwgxssrii0zdf19pyfsxwoqomqvfg6bg8qj3s

DNSLink 👇
ipns://tokens.uniswap.org

JSON 👇
ipfs://bagaaieracglt4ey6qsxtvzqsgwnsw3b6p2tb7nmx5wdgxur2zia7q6nnzh7q

UnixFS JSON 👇
ipfs://bafybeia5ci747h54m2ybc4rf6yqdtm6nzdisxv57pk66fgubjsnnja6wq4

dag-cbor 👇
ipfs://bafyreicnokmhmrnlp2wjhyk2haep4tqxiptwfrp2rrs7rzq7uk766chqvq

dag-json 👇
ipfs://baguqeerasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea

UnixFS image 👇
ipfs://bafybeicklkqcnlvtiscr2hzkubjwnwjinvskffn4xorqeduft3wq7vm5u4

UnixFS video 👇
ipfs://bafybeicq6y27fphdisjtxaybzxold7dczhvxiiyn3bvkyht7b36lveerrm
`
9 changes: 9 additions & 0 deletions examples/helia-browser-verified-fetch/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
Loading
Loading