Skip to content

Commit

Permalink
feat: Customizable UI components (storacha#208)
Browse files Browse the repository at this point in the history
Introduces a new package called `@w3ui/react` that uses the headless
components to build higher level components with some markup and
styling. The markup is not customizable, but @cmunns has been proving
out how we can make the styling customizable using CSS variables in
storacha/w3ui#140

`@w3ui/react` has 3 components that wrap the underlying
react-{keyring|uploader|uploads-list} packages and one that brings them
all together into a component that developers should be able to simply
drop into their applications and start uploading files. 

Signed-off-by: Oli Evans <[email protected]>
Co-authored-by: Alan Shaw <[email protected]>
Co-authored-by: Yusef Napora <[email protected]>
Co-authored-by: Nathan Vander Wilt <[email protected]>
Co-authored-by: Oli Evans <[email protected]>
  • Loading branch information
5 people authored Jan 17, 2023
1 parent f733227 commit 0a776fe
Show file tree
Hide file tree
Showing 30 changed files with 8,439 additions and 570 deletions.
7 changes: 7 additions & 0 deletions docs/react.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# `@w3ui/react`

## Install

```sh
npm install @w3ui/react
```
24 changes: 24 additions & 0 deletions examples/react/playground/.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?
23 changes: 23 additions & 0 deletions examples/react/playground/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# W3UI React Playground

To run this example:

- Clone the w3ui repository and enter the `w3ui` directory

```sh
git clone https://github.com/web3-storage/w3ui
cd w3ui
```

- Install dependencies and build:

```sh
pnpm install
```

- Change to this example directory and run the example:

```sh
cd examples/react/playground
pnpm start
```
13 changes: 13 additions & 0 deletions examples/react/playground/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" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>W3UI React Playground</title>
</head>
<body style="background-color:#1d2027;">
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>
29 changes: 29 additions & 0 deletions examples/react/playground/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@w3ui/example-react-playground",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"start": "vite",
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@w3ui/react-keyring": "workspace:^",
"@w3ui/react": "workspace:^",
"@w3ui/react-uploader": "workspace:^",
"@w3ui/react-uploads-list": "workspace:^",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"react-syntax-highlighter": "^15.5.0"
},
"devDependencies": {
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@vitejs/plugin-react": "^3.0.0",
"vite": "^4.0.0"
}

}
Binary file added examples/react/playground/public/favicon.ico
Binary file not shown.
18 changes: 18 additions & 0 deletions examples/react/playground/src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import logo from './logo.png'
import ContentPage from './ContentPage'
import { KeyringProvider } from '@w3ui/react-keyring'

function App () {
return (
<main>
<header style={{ textAlign: 'center', padding: 10 }}>
<img src={logo} width='250' alt='logo' />
</header>
<KeyringProvider>
<ContentPage />
</KeyringProvider>
</main>
)
}

export default App
178 changes: 178 additions & 0 deletions examples/react/playground/src/ContentPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import React from 'react'
import { UploadsList, Uploader, W3Upload } from '@w3ui/react'
import '@w3ui/react/src/styles/uploader.css'
import { Uploader as UploaderCore, UploaderProvider, useUploader } from '@w3ui/react-uploader'
import { UploadsListProvider } from '@w3ui/react-uploads-list'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { a11yDark } from 'react-syntax-highlighter/dist/esm/styles/prism'

function NoUIUploadComponent () {
const [{ storedDAGShards }, uploader] = useUploader()

return (
<div>
<input type='file' onChange={e => uploader.uploadFile(e.target.files[0])} />
{storedDAGShards?.map(({ cid, size }) => (
<p key={cid.toString()}>
{cid.toString()} ({size} bytes)
</p>
))}
</div>
)
}

function NoUIComponent () {
return (
<UploaderProvider>
<NoUIUploadComponent />
</UploaderProvider>
)
}

function HeadlessUIComponent () {
return (
<UploaderProvider>
<UploaderCore>
<UploaderCore.Form>
<div>
<label htmlFor='file'>File:</label>
<UploaderCore.Input id='file' />
</div>
<button type='submit'>Upload</button>
</UploaderCore.Form>
</UploaderCore>
</UploaderProvider>
)
}

function CustomizableUIComponent () {
return (
<div>
<UploaderProvider>
<Uploader />
</UploaderProvider>
<UploadsListProvider>
<UploadsList />
</UploadsListProvider>
</div>
)
}

function DropinUIComponent () {
return (
<W3Upload />
)
}

export function ContentPage () {
return (
<>
<p style={{ color: 'lightgray', padding: '24px 0', fontSize: '1.2rem', fontWeight: '300' }}>
W3UI provides React components at four different levels of
abstraction, each of which builds upon the last.
</p>

<section>
<h2>1. Drop-in UI</h2>
<p>
Components designed to be dropped into a page with very little customization.
Limited styling customization may be supported:
</p>
<SyntaxHighlighter language='jsx' style={a11yDark}>
{`import { W3Upload } from '@w3ui/react'
function ${DropinUIComponent.name} () {
return (
<W3Upload />
)
}`}
</SyntaxHighlighter>
<DropinUIComponent />
</section>

<section>
<h2>2. Customizable UI</h2>
<p>
Components that work out of the box, and let you customize some aspects of
markup and most of the styling:
</p>
<SyntaxHighlighter language='jsx' style={a11yDark}>
{`import { W3Upload, UploaderProvider } from '@w3ui/react'
function ${CustomizableUIComponent.name}() {
return (
<UploaderProvider>
<Uploader />
</UploaderProvider>
)
}
`}
</SyntaxHighlighter>
<CustomizableUIComponent />
</section>

<section>
<h2>3. Headless UI</h2>
<p>
A set of components designed to work together, modeled after HeadlessUI
components like <a href='https://headlessui.com/react/combobox'>Combobox</a>.
These components don't make any markup or styling choices for you:
</p>
<SyntaxHighlighter language='jsx' style={a11yDark}>
{`import { Uploader } from '@w3ui/react-uploader'
function ${HeadlessUIComponent.name}() {
return (
<Uploader>
<Uploader.Form>
<div>
<label htmlFor='file'>File:</label>
<Uploader.Input id="file" />
</div>
<button type='submit'>Upload</button>
</Uploader.Form>
</Uploader>
)
}`}
</SyntaxHighlighter>
<HeadlessUIComponent />
</section>

<h2>4. No UI</h2>
<p>
Maximum flexibility, React Contexts and hooks only:
</p>
<SyntaxHighlighter language='jsx' style={a11yDark}>
{`import { useUploader, UploaderProvider } from '@w3ui/react-uploader'
function ${NoUIUploadComponent.name}() {
const [{ storedDAGShards }, uploader] = useUploader()
return (
<div>
<input type="file" onChange={e => uploader.uploadFile(e.target.files[0])} />
{storedDAGShards?.map(({ cid, size }) => (
<p key={cid.toString()}>
{cid.toString()} ({size} bytes)
</p>
))}
</div>
)
}
function ${NoUIComponent.name}() {
return (
<UploaderProvider>
<NoUIUploadComponent />
</UploaderProvider>
)
}
`}
</SyntaxHighlighter>
<NoUIComponent />

</>
)
}

export default ContentPage
58 changes: 58 additions & 0 deletions examples/react/playground/src/components/Authenticator.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { useState } from 'react'
import { useKeyring } from '@w3ui/react-keyring'

export default function Authenticator ({ children }) {
const [{ space }, { createSpace, registerSpace, cancelRegisterSpace }] = useKeyring()
const [email, setEmail] = useState('')
const [submitted, setSubmitted] = useState(false)

if (space?.registered()) {
return children
}

if (submitted) {
return (
<div>
<h1 className='near-white'>Verify your email address!</h1>
<p>Click the link in the email we sent to {email} to sign in.</p>
<form onSubmit={e => { e.preventDefault(); cancelRegisterSpace() }}>
<button type='submit' className='ph3 pv2'>Cancel</button>
</form>
</div>
)
}

const handleRegisterSubmit = async e => {
e.preventDefault()
setSubmitted(true)
try {
await createSpace()
await registerSpace(email)
} catch (err) {
throw new Error('failed to register', { cause: err })
} finally {
setSubmitted(false)
}
}

return (
<form onSubmit={handleRegisterSubmit}>
<div className='mb3'>
<label htmlFor='email' className='db mb2'>Email address:</label>
<input id='email' className='db pa2 w-100' type='email' value={email} onChange={e => setEmail(e.target.value)} required />
</div>
<button type='submit' className='ph3 pv2' disabled={submitted}>Register</button>
</form>
)
}

/**
* Wrapping a component with this HoC ensures an identity exists.
*/
export function withIdentity (Component) {
return props => (
<Authenticator>
<Component {...props} />
</Authenticator>
)
}
32 changes: 32 additions & 0 deletions examples/react/playground/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: white;
}

code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

main {
padding: 96px;
max-width: 1024px;
margin: 0 auto;
}

.title {
display: flex;
justify-content: center;
}

section::after {
display: block;
text-align: center;
padding: 64px 0;
content: '⁂';
}
Loading

0 comments on commit 0a776fe

Please sign in to comment.