Skip to content

Commit

Permalink
feat: adds space-finder autocomplete combobox (#268)
Browse files Browse the repository at this point in the history
Riffing on #266 

- add a space-finder combobox based on
https://headlessui.com/react/combobox
- updates w3console layout to look fancy


![w3console](https://user-images.githubusercontent.com/58871/213501204-ef2432f3-c52e-46ed-ac9b-9702771c4259.gif)

Signed-off-by: Oli Evans <[email protected]>
Co-authored-by: Travis Vachon <[email protected]>
  • Loading branch information
olizilla and travis authored Jan 19, 2023
1 parent 0d851a9 commit 3dcd647
Show file tree
Hide file tree
Showing 14 changed files with 823 additions and 241 deletions.
4 changes: 3 additions & 1 deletion examples/react/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"build-storybook": "storybook build"
},
"dependencies": {
"@storybook/addon-actions": "^6.5.15",
"@w3ui/keyring-core": "workspace:^2.0.1",
"@w3ui/react": "workspace:^",
"@w3ui/react-keyring": "workspace:^",
"@w3ui/react-uploader": "workspace:^",
Expand All @@ -22,6 +22,7 @@
"react-syntax-highlighter": "^15.5.0"
},
"devDependencies": {
"@storybook/addon-actions": "^6.5.15",
"@storybook/addon-essentials": "^7.0.0-beta.29",
"@storybook/addon-interactions": "^7.0.0-beta.29",
"@storybook/addon-links": "^7.0.0-beta.29",
Expand All @@ -31,6 +32,7 @@
"@storybook/testing-library": "^0.0.13",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@ucanto/interface": "^4.0.3",
"@vitejs/plugin-react": "^3.0.0",
"@w3ui/uploads-list-core": "workspace:^",
"multiformats": "^10.0.2",
Expand Down
38 changes: 38 additions & 0 deletions examples/react/playground/src/stories/SpaceList.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { DID } from '@ucanto/interface'
import type { KeyringContextValue } from '@w3ui/react-keyring'

import React from 'react'
import { SpaceList } from '@w3ui/react'
import { Space } from '@w3ui/keyring-core'
import { KeyringContext, keyringContextDefaultValue } from '@w3ui/react-keyring'

function contextValue (state = {}, actions = {}): KeyringContextValue {
return [
{ ...keyringContextDefaultValue[0], ...state },
{ ...keyringContextDefaultValue[1], ...actions }
]
}

function WrappedSpaceList ({ spaceDIDs = [], setCurrentSpace }: { spaceDIDs: DID[], setCurrentSpace: any }): JSX.Element {
const spaces = spaceDIDs.map(did => new Space(did, {}))
return (
<KeyringContext.Provider value={contextValue({ spaces }, { setCurrentSpace })}>
<SpaceList />
</KeyringContext.Provider>
)
}

export default {
title: 'w3ui/SpaceList',
component: WrappedSpaceList,
tags: ['autodocs'],
argTypes: {
setCurrentSpace: { action: 'set space' }
}
}

export const Primary = {
args: {
spaceDIDs: ['did:example:abc123']
}
}
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": {
"@w3ui/keyring-core": "workspace:^2.0.1",
"@w3ui/react": "workspace:^",
"@w3ui/react-keyring": "workspace:^",
"@w3ui/react-uploads-list": "workspace:^2.0.1",
Expand Down
192 changes: 167 additions & 25 deletions examples/react/w3console/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,193 @@
import { Authenticator, Uploader, UploadsList, W3APIProvider } from '@w3ui/react'
import { ChangeEvent, useEffect, useState } from 'react'
import type { Space } from '@w3ui/keyring-core'

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

function Space (): JSX.Element {
function SpaceRegistrar (): JSX.Element {
const [, { registerSpace }] = useKeyring()
const [email, setEmail] = useState('')
const [submitted, setSubmitted] = useState(false)
async function onSubmit (e: React.FormEvent<HTMLFormElement>): Promise<void> {
e.preventDefault()
setSubmitted(true)
try {
await registerSpace(email)
} catch (err) {
console.log(err)
throw new Error('failed to register', { cause: err })
} finally {
setSubmitted(false)
}
}
return (
<div>
{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>
</>
)}
</div>
)
}

function SpaceSection (): JSX.Element {
const [{ space }] = useKeyring()
const [, { reload }] = useUploadsList()
// reload the uploads list when the space changes
// TODO: this currently does a network request - we'd like to just reset
// to the latest state we have and revalidate in the background (SWR)
// but it's not clear how all that state should work yet - perhaps
// we need some sort of state management primitive in the uploads list?
useEffect(() => { void reload() }, [space])
const registered = Boolean(space?.registered())
return (
<div className='container mx-auto'>
<div className='flex flex-row space-x-4 mb-4 justify-between'>
<div className='shrink-0'>
{(space !== undefined) && (
<img src={`https://www.gravatar.com/avatar/${md5(space.did())}?d=identicon`} className='w-20' />
)}
</div>
<Uploader onUploadComplete={() => { void reload() }} />
<div>
<header className='py-3'>
{(space !== undefined) && (
<div className='flex flex-row items-start gap-2'>
<img title={space.did()} src={`https://www.gravatar.com/avatar/${md5(space.did())}?d=identicon`} className='w-10 hover:saturate-200 saturate-0 invert border-solid border-gray-500 border' />
<div>
<h1 className='text-xl font-semibold leading-5'>{space.name() ?? 'Untitled'}</h1>
<label className='font-mono text-xs text-gray-500'>{space.did()}</label>
</div>
</div>
)}

</header>
<div className='container mx-auto'>
{registered
? (
<>
<Uploader onUploadComplete={() => { void reload() }} />
<div className='mt-8'>
<UploadsList />
</div>
</>
)
: (
<SpaceRegistrar />
)}
</div>
<UploadsList />
</div>
)
}

function SpaceCreator (props: any): JSX.Element {
const [, { createSpace, registerSpace }] = useKeyring()
const [creating, setCreating] = useState(false)
const [submitted, setSubmitted] = useState(false)
const [email, setEmail] = useState('')
const [name, setName] = useState('')

async function onSubmit (e: React.FormEvent<HTMLFormElement>): Promise<void> {
e.preventDefault()
setSubmitted(true)
try {
await createSpace(name)
await registerSpace(email)
} catch (err) {
console.log(err)
throw new Error('failed to register', { cause: err })
} finally {
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
? (
<div>creating space...</div>
)
: (
<button className='w3ui-button py-2' onClick={() => setCreating(true)}>
Add Space
</button>
)}
</div>
)
}

function SpaceSelector (props: any): JSX.Element {
const [{ space: currentSpace, spaces }, { setCurrentSpace }] = useKeyring()
async function selectSpace (space: Space): Promise<void> {
await setCurrentSpace(space.did())
}
return (
<div>
<h3 className='text-xs tracking-wider uppercase font-bold my-2 text-gray-400 font-mono'>Spaces</h3>
<SpaceFinder spaces={spaces} selected={currentSpace} setSelected={(space: Space) => { void selectSpace(space) }} />
</div>
)
}

export function Logo (): JSX.Element {
return (
<h1 className='font-bold flex flex-row justify-center items-center gap-2'>
<svg className='site-logo-image text-white' width='30' viewBox='0 0 27.2 27.18' xmlns='http://www.w3.org/2000/svg'><path d='M13.6 27.18A13.59 13.59 0 1127.2 13.6a13.61 13.61 0 01-13.6 13.58zM13.6 2a11.59 11.59 0 1011.6 11.6A11.62 11.62 0 0013.6 2z' fill='currentColor' /><path d='M12.82 9.9v2.53h1.6V9.9l2.09 1.21.77-1.21-2.16-1.32 2.16-1.32-.77-1.21-2.09 1.21V4.73h-1.6v2.53l-2-1.21L10 7.26l2.2 1.32L10 9.9l.78 1.21zM18 17.79v2.52h1.56v-2.52L21.63 19l.78-1.2-2.16-1.33 2.16-1.28-.78-1.19-2.08 1.2v-2.58H18v2.56L15.9 14l-.77 1.2 2.16 1.32-2.16 1.33.77 1.15zM8.13 17.79v2.52h1.56v-2.52L11.82 19l.77-1.2-2.16-1.33 2.12-1.28-.73-1.24-2.13 1.23v-2.56H8.13v2.56L6.05 14l-.78 1.2 2.16 1.3-2.16 1.33.78 1.17z' fill='currentColor' /></svg>
console
</h1>
)
}

export function App (): JSX.Element {
return (
<W3APIProvider>
<Authenticator>
<div className='flex min-h-full w-full'>
<nav className='flex-none w-72 bg-white p-4 border-r border-gray-200'>
<nav className='flex-none w-64 bg-gray-900 text-white px-4 pb-4 border-r border-gray-800'>
<div className='flex flex-col justify-between min-h-full'>
<div className='grow'>
<h1 className='font-bold pb-4 flex flex-row justify-start items-center gap-2'>
<svg className='site-logo-image text-black' width='30' viewBox='0 0 27.2 27.18' xmlns='http://www.w3.org/2000/svg'><path d='M13.6 27.18A13.59 13.59 0 1127.2 13.6a13.61 13.61 0 01-13.6 13.58zM13.6 2a11.59 11.59 0 1011.6 11.6A11.62 11.62 0 0013.6 2z' fill='currentColor' /><path d='M12.82 9.9v2.53h1.6V9.9l2.09 1.21.77-1.21-2.16-1.32 2.16-1.32-.77-1.21-2.09 1.21V4.73h-1.6v2.53l-2-1.21L10 7.26l2.2 1.32L10 9.9l.78 1.21zM18 17.79v2.52h1.56v-2.52L21.63 19l.78-1.2-2.16-1.33 2.16-1.28-.78-1.19-2.08 1.2v-2.58H18v2.56L15.9 14l-.77 1.2 2.16 1.32-2.16 1.33.77 1.15zM8.13 17.79v2.52h1.56v-2.52L11.82 19l.77-1.2-2.16-1.33 2.12-1.28-.73-1.24-2.13 1.23v-2.56H8.13v2.56L6.05 14l-.78 1.2 2.16 1.3-2.16 1.33.78 1.17z' fill='currentColor' /></svg>
console
</h1>
<div class='flex-none'>
<SpaceSelector />
</div>
<div className='flex-none'>
Space selector
<ul>
<li>space 1</li>
<li className='font-bold'>space 2</li>
</ul>
<div>
<SpaceCreator className='mb-4' />
<Logo />
</div>
</div>
</nav>
<main className='grow bg-gray-100 dark:bg-dark-gray p-4'>
<Space />
<main className='grow bg-dark-gray text-white p-4'>
<SpaceSection />
</main>
</div>
</Authenticator>
Expand Down
8 changes: 4 additions & 4 deletions examples/react/w3console/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

:root {
--w3ui-uploader-height: theme(spacing.36);
--w3ui-uploader-primary: theme(colors.orange.400);
--w3ui-uploader-primary-hover: theme(colors.orange.500);
--w3ui-uploader-primary: theme(colors.slate.800);
--w3ui-uploader-primary-hover: theme(colors.blue.900);
}

.w3-uploads-list {
Expand All @@ -21,15 +21,15 @@
}

.w3-uploads-list thead {
@apply text-left bg-gray-400 dark:bg-gray-900 bg-opacity-50 text-sm;
@apply text-left bg-opacity-50 text-xs tracking-wide text-zinc-300;
}

.w3-uploads-list th {
@apply p-3;
}

.w3-uploads-list td {
@apply block w-64 p-3;
@apply block w-64 p-2 pl-3 font-mono text-xs;
}

.w3-uploads-list nav {
Expand Down
3 changes: 2 additions & 1 deletion examples/react/w3console/tailwind.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
module.exports = {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}'
'./src/**/*.{js,ts,jsx,tsx}',
'./node_modules/@w3ui/react/src/*.{js,ts,jsx,tsx}'
],
theme: {
extend: {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"serve:examples": "serve examples"
},
"devDependencies": {
"@babel/core": "^7.20.12",
"@babel/plugin-transform-modules-commonjs": "^7.18.6",
"@babel/preset-env": "^7.19.0",
"@babel/preset-react": "^7.18.6",
Expand Down
12 changes: 11 additions & 1 deletion packages/keyring-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class Space implements Principal {
* The given space name.
*/
name (): string | undefined {
return this.#meta.name == null ? String(this.#meta.name) : undefined
return this.#meta.name != null ? String(this.#meta.name) : undefined
}

/**
Expand All @@ -43,6 +43,16 @@ export class Space implements Principal {
meta (): Record<string, any> {
return this.#meta
}

/**
* Compares this space's DID to `space`'s DID, returns
* true if they are the same, false otherwise.
* If `space` is null or undefined, returns false since
* this space is neither.
*/
sameAs (space?: Space): boolean {
return this.did() === space?.did()
}
}

export interface KeyringContextState {
Expand Down
2 changes: 2 additions & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
},
"homepage": "https://github.com/web3-storage/w3ui/tree/main/packages/react-ui",
"dependencies": {
"@headlessui/react": "^1.7.7",
"@heroicons/react": "^2.0.13",
"@w3ui/react-keyring": "workspace:^",
"@w3ui/react-uploader": "workspace:^",
"@w3ui/react-uploads-list": "workspace:^",
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/Authenticator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export function AuthenticationSubmitted (): JSX.Element {
}

export function AuthenticationEnsurer ({ children }: { children: JSX.Element | JSX.Element[] }): JSX.Element {
const [{ space, submitted }] = useAuthenticator()
const registered = Boolean(space?.registered())
const [{ spaces, submitted }] = useAuthenticator()
const registered = Boolean(spaces.some(s => s.registered()))
if (registered) {
return <>{children}</>
} else if (submitted) {
Expand Down
Loading

0 comments on commit 3dcd647

Please sign in to comment.