Skip to content

Commit

Permalink
fix: w3console polish (#284)
Browse files Browse the repository at this point in the history
first up: improved space creation flow

There are few different ways this could work, but the fact that we store
`currentSpace` in the Agent combined with the fact that that
`currentSpace` is updated automatically when we call `createSpace` makes
it harder to implement some designs.

Put another way, it would be hard to have a space creator that does not
change the currently selected space, so these changes lean into the way
state management works right now.


https://user-images.githubusercontent.com/1113/213601254-86fd1114-e6d3-45a6-add3-44100166904a.mp4
  • Loading branch information
travis authored Jan 20, 2023
1 parent 3dcd647 commit 9a67365
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 94 deletions.
1 change: 1 addition & 0 deletions examples/react/w3console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@heroicons/react": "^2.0.13",
"@w3ui/keyring-core": "workspace:^2.0.1",
"@w3ui/react": "workspace:^",
"@w3ui/react-keyring": "workspace:^",
Expand Down
122 changes: 73 additions & 49 deletions examples/react/w3console/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { ChangeEvent, useEffect, useState } from 'react'
import type { ChangeEvent } from 'react'
import type { Space } from '@w3ui/keyring-core'

import { useEffect, useState } from 'react'
import { Authenticator, Uploader, UploadsList, W3APIProvider, SpaceFinder } from '@w3ui/react'
import { useKeyring } from '@w3ui/react-keyring'
import { useUploadsList } from '@w3ui/react-uploads-list'
import { ArrowPathIcon } from '@heroicons/react/20/solid'
import md5 from 'blueimp-md5'
import '@w3ui/react/src/styles/uploader.css'

function SpaceRegistrar (): JSX.Element {
const [, { registerSpace }] = useKeyring()
const [email, setEmail] = useState('')
const [submitted, setSubmitted] = useState(false)
function resetForm (): void {
setEmail('')
}
async function onSubmit (e: React.FormEvent<HTMLFormElement>): Promise<void> {
e.preventDefault()
setSubmitted(true)
Expand All @@ -20,35 +25,40 @@ function SpaceRegistrar (): JSX.Element {
console.log(err)
throw new Error('failed to register', { cause: err })
} finally {
resetForm()
setSubmitted(false)
}
}
return (
<div>
{submitted
? (
<div className='flex flex-col items-center space-y-24 pt-12'>
<div className='flex flex-col items-center space-y-2'>
<h3 className='text-lg'>Verify your email address!</h3>
<p>Click the link in the email we sent to start uploading to this space.</p>
</div>
<div className='flex flex-col items-center space-y-4'>
<h5>
Need a new verification email?
</h5>
<form
className='flex flex-col items-center space-y-2'
onSubmit={(e: React.FormEvent<HTMLFormElement>) => { void onSubmit(e) }}
>
<input
className='text-black px-2 py-1 rounded'
type='email' placeholder='Email'
value={email}
onChange={(e: ChangeEvent<HTMLInputElement>) => { setEmail(e.target.value) }}
/>
<input
type='submit' className='w3ui-button' value='Re-send Verification Email'
disabled={email === ''}
/>
</form>
{submitted &&
<p>
Please check your email for a verification email.
</p>
)
: (
<>
<p>
Before you upload files, you must register this space.
</p>
<form onSubmit={(e: React.FormEvent<HTMLFormElement>) => { void onSubmit(e) }}>
<input
type='email' placeholder='Email' autofocus
value={email}
onChange={(e: ChangeEvent<HTMLInputElement>) => { setEmail(e.target.value) }}
/>
<input
type='submit' className='w3ui-button' value='Register'
disabled={email === ''}
/>
</form>
</>
)}
Verification re-sent, please check your email for a verification email.
</p>}
</div>
</div>
)
}
Expand Down Expand Up @@ -102,48 +112,62 @@ function SpaceCreator (props: any): JSX.Element {
const [email, setEmail] = useState('')
const [name, setName] = useState('')

function resetForm (): void {
setEmail('')
setName('')
}

async function onSubmit (e: React.FormEvent<HTMLFormElement>): Promise<void> {
e.preventDefault()
setSubmitted(true)
try {
await createSpace(name)
await registerSpace(email)
// ignore this because the UI knows how to help the user recover
// from space registration failure
void registerSpace(email)
} catch (err) {
console.log(err)
throw new Error('failed to register', { cause: err })
} finally {
resetForm()
setSubmitted(false)
}
}
return (
<div {...props}>
{(creating)
? (
<form onSubmit={(e: React.FormEvent<HTMLFormElement>) => { void onSubmit(e) }}>
<input
className='text-black'
type='email' placeholder='Email' autofocus
value={email}
onChange={(e: ChangeEvent<HTMLInputElement>) => { setEmail(e.target.value) }}
/>
<input
className='text-black'
placeholder='Name'
value={name}
onChange={(e: ChangeEvent<HTMLInputElement>) => { setName(e.target.value) }}
/>
<input type='submit' className='w3ui-button' value='Create' />
</form>
)
: submitted
? submitted
? (
<div>creating space...</div>
<div className='flex flex-col items-center space-y-4'>
<h5>Creating Space...</h5>
<ArrowPathIcon className='animate-spin w-6' />
</div>
)
: (
<button className='w3ui-button py-2' onClick={() => setCreating(true)}>
Add Space
</button>
)}
<form
className='flex flex-col space-y-2'
onSubmit={(e: React.FormEvent<HTMLFormElement>) => { void onSubmit(e) }}
>
<input
className='text-black py-1 px-2 rounded'
type='email' placeholder='Email' autofocus
value={email}
onChange={(e: ChangeEvent<HTMLInputElement>) => { setEmail(e.target.value) }}
/>
<input
className='text-black py-1 px-2 rounded'
placeholder='Name'
value={name}
onChange={(e: ChangeEvent<HTMLInputElement>) => { setName(e.target.value) }}
/>
<input type='submit' className='w3ui-button' value='Create' />
</form>
)
: (
<button className='w3ui-button py-2' onClick={() => setCreating(true)}>
Add Space
</button>
)}
</div>
)
}
Expand Down
4 changes: 2 additions & 2 deletions examples/react/w3console/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
}

.w3ui-uploader__file {
@apply rounded-md w-full grid grid-cols-2 p-4 bg-gray-200 dark:bg-gray-900;
@apply rounded-md w-full grid grid-cols-2 p-4 bg-gray-200 text-gray-500 dark:bg-gray-900 dark:text-white;
}

.w3ui-uploader__file .name {
Expand All @@ -54,7 +54,7 @@
}

.w3ui-uploader-console {
@apply rounded-md w-full bg-gray-200 dark:bg-gray-900 p-4 truncate;
@apply rounded-md w-full bg-gray-200 text-gray-500 dark:bg-gray-900 dark:text-white p-4 truncate;
}

.w3ui-simple-authenticator-verify-email {
Expand Down
9 changes: 7 additions & 2 deletions packages/react-uploader/src/Uploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export type UploaderComponentContextActions = UploaderContextActions & {
* Set a file to be uploaded to web3.storage. The file will be uploaded
* when `handleUploadSubmit` is called.
*/
setFile: React.Dispatch<React.SetStateAction<File | undefined>>
setFile: (file?: File) => void
}

export type UploaderComponentContextValue = [
Expand Down Expand Up @@ -93,6 +93,11 @@ export const UploaderRoot: Component<UploaderRootProps> = createComponent((props
const [status, setStatus] = useState(Status.Idle)
const [error, setError] = useState()

const setFileAndReset = (file?: File): void => {
setFile(file)
setStatus(Status.Idle)
}

const handleUploadSubmit = async (e: Event): Promise<void> => {
e.preventDefault()
if (file != null) {
Expand All @@ -114,7 +119,7 @@ export const UploaderRoot: Component<UploaderRootProps> = createComponent((props

const uploaderComponentContextValue = useMemo<UploaderComponentContextValue>(() => [
{ ...uploaderState, file, dataCID, status, error, handleUploadSubmit },
{ ...uploaderActions, setFile }
{ ...uploaderActions, setFile: setFileAndReset }
], [uploaderState, file, dataCID, status, error, handleUploadSubmit, uploaderActions, setFile])

return (
Expand Down
82 changes: 45 additions & 37 deletions packages/react/src/Uploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,45 +29,68 @@ interface DoneProps {
storedDAGShards?: CARMetadata[]
}

export const Done = ({ file, dataCID, storedDAGShards }: DoneProps): JSX.Element => {
export const Done = ({ dataCID }: DoneProps): JSX.Element => {
const [, { setFile }] = useUploaderComponent()
const cid: string = dataCID?.toString() ?? ''
return (
<div className='done'>
<h1 className='title'>Done!</h1>
<p className='cid'>{cid}</p>
<p className='view'><a href={`https://${cid}.ipfs.w3s.link/`}>View {file?.name} on IPFS Gateway.</a></p>
<h5 className='chunks'>Shards ({storedDAGShards?.length}):</h5>
{storedDAGShards?.map(({ cid, size }) => (
<p className='shard' key={cid.toString()}>
{cid.toString()} ({size} bytes)
</p>
))}
<p className='cid'>
Uploaded to <a href={`https://${cid}.ipfs.w3s.link/`}>{cid}</a>
</p>
<button
className='w3ui-button'
onClick={() => { setFile(undefined) }}
>
Add More
</button>
</div>
)
}

const UploaderForm = (): JSX.Element => {
const [{ file }] = useUploaderComponent()
const [{ status, file }] = useUploaderComponent()
const hasFile = (file !== undefined)
return (
<UploaderCore.Form>
<div className='w3ui-uploader'>
<div className={`w3ui-uploader ${status} ${hasFile ? 'has-file' : 'no-file'}`}>
<label className='w3ui-uploader__label'>File:</label>
<UploaderCore.Input className='w3ui-uploader__input' />
<UploaderContents />
</div>
{(file !== undefined) && (
<div className='w3ui-uploader__file'>
<span className='name'>{file.name}</span>
<span className='type'>{file.type}</span>
<span className='size'>{file.size}</span>
</div>
)}
<button type='submit' className='w3ui-button' disabled={file === undefined}>
Upload
</button>
</UploaderCore.Form>
)
}

const UploaderContents = (): JSX.Element => {
const [{ status, file }] = useUploaderComponent()
const hasFile = (file !== undefined)
if (status === Status.Idle) {
if (hasFile) {
return (
<>
<div className='w3ui-uploader__file'>
<span className='name'>{file.name}</span>
<span className='type'>{file.type}</span>
<span className='size'>{file.size}</span>
</div>
<button type='submit' className='w3ui-button' disabled={file === undefined}>
Upload
</button>
</>
)
} else {
return <></>
}
} else {
return (
<div className='w3ui-uploader-console'>
<UploaderConsole />
</div>
)
}
}

const UploaderConsole = (): JSX.Element => {
const [{ status, file, error, dataCID, storedDAGShards }] = useUploaderComponent()
switch (status) {
Expand All @@ -84,29 +107,14 @@ const UploaderConsole = (): JSX.Element => {
}
}

const UploaderBody = (): JSX.Element => {
const [{ status }] = useUploaderComponent()

return (
<>
<UploaderForm />
{(status !== Status.Idle) && (
<div className='w3ui-uploader-console'>
<UploaderConsole />
</div>
)}
</>
)
}

export interface SimpleUploaderProps {
onUploadComplete?: OnUploadComplete
}

export const Uploader = ({ onUploadComplete }: SimpleUploaderProps): JSX.Element => {
return (
<UploaderCore as='div' className='w3ui-uploader-wrapper' onUploadComplete={onUploadComplete}>
<UploaderBody />
<UploaderForm />
</UploaderCore>
)
}
6 changes: 5 additions & 1 deletion packages/react/src/UploadsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ function Uploads ({ uploads }: { uploads?: UploadListResult[] }): JSX.Element {
<tbody>
{uploads.map(({ root }) => (
<tr key={root.toString()}>
<td>{root.toString()}</td>
<td>
<a href={`https://${root.toString()}.ipfs.w3s.link/`}>
{root.toString()}
</a>
</td>
</tr>
))}
</tbody>
Expand Down
Loading

0 comments on commit 9a67365

Please sign in to comment.