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: adds space-finder autocomplete combobox #268

Merged
merged 10 commits into from
Jan 19, 2023
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
37 changes: 37 additions & 0 deletions examples/react/playground/src/stories/SpaceList.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Space } from '@w3ui/keyring-core'
import type { DID } from '@ucanto/interface'

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

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

function WrappedSpaceList ({ spaceDIDs = [], setCurrentSpace }: { spaceDIDs: DID[], setCurrentSpace: any }) {
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
190 changes: 165 additions & 25 deletions examples/react/w3console/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,191 @@
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={selectSpace} />
</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
18 changes: 13 additions & 5 deletions packages/keyring-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export class Space implements Principal {
* The given space name.
*/
name (): string | undefined {
return this.#meta.name == null ? String(this.#meta.name) : undefined
// TODO: I think this was a typo, please review carefully!
return this.#meta.name != null ? String(this.#meta.name) : undefined
}

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

// TODO: is this the right name for this function?
// TODO: needs docs once settled on name and API
sameAs (space?: Space): boolean {
return this.did() === space?.did()
}
}

export interface KeyringContextState {
Expand Down Expand Up @@ -84,11 +91,12 @@ export interface KeyringContextActions {
*/
setCurrentSpace: (did: DID) => Promise<void>
/**
* Register the current space, verify the email address and store in secure
* storage. Use cancelRegisterSpace to abort. Automatically sets the
* newly registered space as the current space.
* Register a space (current space by default), verify the email
* address and store in secure storage. Use cancelRegisterSpace
* to abort. Automatically sets the newly registered space
* as the current space.
*/
registerSpace: (email: string) => Promise<void>
registerSpace: (email: string, did?: DID) => Promise<void>
/**
* Abort an ongoing account registration.
*/
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