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

wip: first pass at adding a space selector to w3console #266

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
169 changes: 150 additions & 19 deletions examples/react/w3console/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,160 @@
import { ChangeEvent, useEffect, useState } from 'react'
import type { Space } from '@w3ui/keyring-core'

import { Authenticator, Uploader, UploadsList, W3APIProvider } 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' />
{registered
? (
<>
<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>
<UploadsList />
</>
)
: (
<SpaceRegistrar />
)}
</div>
<Uploader onUploadComplete={() => { void reload() }} />
</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
type='email' placeholder='Email' autofocus
value={email}
onChange={(e: ChangeEvent<HTMLInputElement>) => { setEmail(e.target.value) }}
/>
<input
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 {...props}>
<h3 className='text-lg uppercase font-bold my-4'>Spaces</h3>
<ul className='space-y-2'>
{spaces.map((space, i) => (
<li key={space.did()} className={`hover:font-bold ${space.sameAs(currentSpace) ? 'font-bold' : ''}`}>
<button onClick={() => { void selectSpace(space) }}>
{space.name() ?? `Space ${i + 1}`}
</button>
</li>
))}
</ul>
<SpaceCreator className='mt-12' />
</div>
)
}
Expand All @@ -28,24 +165,18 @@ export function App (): JSX.Element {
<Authenticator>
<div className='flex min-h-full w-full'>
<nav className='flex-none w-72 bg-white p-4 border-r border-gray-200'>
<div className='flex flex-col justify-between min-h-full'>
<div className='grow'>
<div className='flex flex-col justify-start min-h-full'>
<div>
<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>
<div className='flex-none'>
Space selector
<ul>
<li>space 1</li>
<li className='font-bold'>space 2</li>
</ul>
</div>
<SpaceSelector className='flex-none grow' />
</div>
</nav>
<main className='grow bg-gray-100 dark:bg-dark-gray p-4'>
<Space />
<SpaceSection />
</main>
</div>
</Authenticator>
Expand Down
2 changes: 1 addition & 1 deletion examples/react/w3console/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
}

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

.w3-uploads-list th {
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
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
22 changes: 22 additions & 0 deletions packages/react/src/SpacePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Space } from '@w3ui/keyring-core'

import React from 'react'
import { useKeyring } from '@w3ui/react-keyring'

export function SpaceList (props: any): JSX.Element {
const [{ space: currentSpace, spaces }, { setCurrentSpace }] = useKeyring()
async function selectSpace (space: Space): Promise<void> {
await setCurrentSpace(space.did())
}
return (
<ul {...props}>
{spaces.map((space, i) => (
<li key={space.did()} className={`hover:font-bold ${space.sameAs(currentSpace) ? 'font-bold' : ''}`}>
<button onClick={() => { void selectSpace(space) }}>
{space.name() ?? `Space ${i + 1}`}
</button>
</li>
))}
</ul>
)
}
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './Uploader'
export * from './UploadsList'
export * from './Authenticator'
export * from './W3Upload'
export * from './SpacePicker'
export * from './providers/W3API'
Loading