Skip to content

Commit

Permalink
feat: support multi file upload and directory wrapping (#615)
Browse files Browse the repository at this point in the history
Make backwards compatible API changes to support multi-file upload and
directory wrapping.

Also support directory selection, wrapping the wonky builtin API with
something a little cleaner.

Revive the multi file upload and uploads lists examples!
  • Loading branch information
travis authored Dec 20, 2023
1 parent c2ca4b6 commit a924abf
Show file tree
Hide file tree
Showing 28 changed files with 779 additions and 49 deletions.
53 changes: 32 additions & 21 deletions examples/react/components/src/Uploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ function humanFileSize (bytes: number): string {

interface UploadingProps {
file?: File
files?: File[]
storedDAGShards: CARMetadata[]
uploadProgress: UploadProgress
}

function Uploading ({ file, storedDAGShards, uploadProgress }: UploadingProps): ReactNode {
function Uploading ({ file, files, storedDAGShards, uploadProgress }: UploadingProps): ReactNode {
const fileName = ((files != null) && files.length > 1) ? 'your files' : file?.name
return (
<div className='flex flex-col items-center w-full'>
<h1 className='font-bold text-sm uppercase text-zinc-950'>Uploading {file?.name}</h1>
<h1 className='font-bold text-sm uppercase text-zinc-950'>Uploading {fileName}</h1>
<UploadLoader uploadProgress={uploadProgress} />
{storedDAGShards?.map(({ cid, size }) => (
<p className='text-xs max-w-full overflow-hidden text-ellipsis' key={cid.toString()}>
Expand All @@ -40,20 +42,22 @@ function Errored ({ error }: { error?: Error }): ReactNode {

interface DoneProps {
file?: File
files?: File[]
dataCID?: AnyLink
storedDAGShards: CARMetadata[]
}

const Done = ({ file, dataCID, storedDAGShards }: DoneProps): ReactNode => {
const Done = ({ file, files, dataCID, storedDAGShards }: DoneProps): ReactNode => {
const cidString: string = dataCID?.toString() ?? ''
const fileName = ((files != null) && files.length > 1) ? 'your files' : file?.name
return (
<div>
<h1 className='near-white'>Done!</h1>
<p className='f6 code truncate'>{cidString}</p>
<p><a href={`https://w3s.link/ipfs/${cidString}`} className='blue'>View {file?.name} on IPFS Gateway.</a></p>
<p className='near-white'>Chunks ({storedDAGShards.length}):</p>
<h1 className='text-gray-800'>Done!</h1>
<p className='truncate'>{cidString}</p>
<p><a href={`https://w3s.link/ipfs/${cidString}`} className='text-blue-800'>View {fileName} on IPFS Gateway.</a></p>
<p className='text-gray-800'>Chunks ({storedDAGShards.length}):</p>
{storedDAGShards.map(({ cid, size }) => (
<p key={cid.toString()} className='f7 truncate'>
<p key={cid.toString()} className='truncate'>
{cid.toString()} ({size} bytes)
</p>
))}
Expand All @@ -62,16 +66,16 @@ const Done = ({ file, dataCID, storedDAGShards }: DoneProps): ReactNode => {
}

function UploaderConsole (): ReactNode {
const [{ status, file, error, dataCID, storedDAGShards, uploadProgress }] =
const [{ status, file, files, error, dataCID, storedDAGShards, uploadProgress }] =
useUploader()

switch (status) {
case UploadStatus.Uploading: {
return <Uploading file={file} storedDAGShards={storedDAGShards} uploadProgress={uploadProgress} />
return <Uploading file={file} files={files} storedDAGShards={storedDAGShards} uploadProgress={uploadProgress} />
}
case UploadStatus.Succeeded: {
return (
<Done file={file} dataCID={dataCID} storedDAGShards={storedDAGShards} />
<Done file={file} files={files} dataCID={dataCID} storedDAGShards={storedDAGShards} />
)
}
case UploadStatus.Failed: {
Expand All @@ -84,19 +88,21 @@ function UploaderConsole (): ReactNode {
}

function UploaderContents (): ReactNode {
const [{ status, file }] = useUploader()
const [{ status, file, files }] = useUploader()
const hasFile = file !== undefined
if (status === UploadStatus.Idle) {
return hasFile
? (
<>
<div className='flex flex-row'>
<div className='flex flex-col justify-around'>
<span className='text-sm'>{file.name}</span>
<span className='text-xs text-white/75 font-mono'>
{humanFileSize(file.size)}
</span>
</div>
<div className='flex flex-row space-x-2 flex-wrap max-w-xl'>
{files?.map((f, i) => (
<div className='flex flex-col justify-around' key={i}>
<span className='text-sm'>{f.name}</span>
<span className='text-xs text-white/75 font-mono'>
{humanFileSize(f.size)}
</span>
</div>
))}
</div>
<div className='p-4'>
<button
Expand All @@ -119,14 +125,19 @@ function UploaderContents (): ReactNode {
}
}

export function UploaderForm (): ReactNode {
interface UploaderFormProps {
multiple?: boolean
allowDirectory?: boolean
}

export function UploaderForm ({ multiple, allowDirectory }: UploaderFormProps): ReactNode {
const [{ file }] = useUploader()
const hasFile = file !== undefined
return (
<Uploader.Form className="m-12">
<div className='relative shadow h-52 p-8 rounded-md bg-white/5 hover:bg-white/20 border-2 border-dotted border-zinc-950 flex flex-col justify-center items-center text-center'>
<label className={`${hasFile ? 'hidden' : 'block h-px w-px overflow-hidden absolute whitespace-nowrap'}`}>File:</label>
<Uploader.Input className={`${hasFile ? 'hidden' : 'block absolute inset-0 cursor-pointer w-full opacity-0'}`} />
<Uploader.Input multiple={multiple} allowDirectory={allowDirectory} className={`${hasFile ? 'hidden' : 'block absolute inset-0 cursor-pointer w-full opacity-0'}`} />
<UploaderContents />
{hasFile ? '' : <span>Drag files or Click to Browse</span>}
</div>
Expand Down
20 changes: 20 additions & 0 deletions examples/react/multi-file-upload/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
24 changes: 24 additions & 0 deletions examples/react/multi-file-upload/.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?
8 changes: 8 additions & 0 deletions examples/react/multi-file-upload/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# React + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
13 changes: 13 additions & 0 deletions examples/react/multi-file-upload/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" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>W3UI Multi File Upload Example App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
32 changes: 32 additions & 0 deletions examples/react/multi-file-upload/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@w3ui/example-react-multi-file-upload",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"start": "vite",
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@heroicons/react": "^2.0.17",
"@w3ui/example-react-components": "workspace:^",
"@w3ui/react": "workspace:^",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.13",
"eslint": "^8.53.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"postcss": "^8.4.21",
"tailwindcss": "^3.2.4",
"vite": "^5.0.0"
}
}
6 changes: 6 additions & 0 deletions examples/react/multi-file-upload/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
43 changes: 43 additions & 0 deletions examples/react/multi-file-upload/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Authenticator, Provider, Uploader, WrapInDirectoryCheckbox, useUploader } from '@w3ui/react'
import { AuthenticationEnsurer, SpaceEnsurer, UploaderForm } from '@w3ui/example-react-components'
import React, { useState } from 'react'

function Options ({ allowDirectory, setAllowDirectory }) {
const [{ files }] = useUploader()
return (
<div className='flex flex-col'>
{(files?.length === 1) && (
<label className='flex space-x-2'>
<WrapInDirectoryCheckbox />
<span>Wrap in directory?</span>
</label>
)}
<label className='flex space-x-2'>
<input type='checkbox' checked={allowDirectory} onChange={e => setAllowDirectory(e.target.checked)} />
<span>Allow directory selection?</span>
</label>
</div>
)
}

function App () {
const [allowDirectory, setAllowDirectory] = useState(false)
return (
<div className='bg-grad flex flex-col items-center h-screen'>
<Provider>
<Authenticator>
<AuthenticationEnsurer>
<SpaceEnsurer>
<Uploader>
<UploaderForm multiple allowDirectory={allowDirectory} />
<Options allowDirectory={allowDirectory} setAllowDirectory={setAllowDirectory} />
</Uploader>
</SpaceEnsurer>
</AuthenticationEnsurer>
</Authenticator>
</Provider>
</div>
)
}

export default App
26 changes: 26 additions & 0 deletions examples/react/multi-file-upload/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
.w3ui-button {
@apply text-white bg-slate-800 hover:bg-blue-800 rounded-sm py-2 px-8 text-sm font-medium transition-colors ease-in;
}

.w3ui-button.active {
@apply bg-transparent;
}

.authenticator {
@apply bg-zinc-950 w-full h-screen flex flex-col justify-center items-center;
}
}

@layer utilities {
.bg-grad {
background-color: rgb(244 244 245);
background-image: linear-gradient(225deg, #ff149296 0%, #00ffff96 100%);
background-size: 400% 400%;
animation: bgPosDrift 60s ease infinite;
}
}
10 changes: 10 additions & 0 deletions examples/react/multi-file-upload/src/main.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
13 changes: 13 additions & 0 deletions examples/react/multi-file-upload/tailwind.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
"../components/src/**/*.{js,ts,jsx,tsx}"
],
theme: {
extend: {},
},
plugins: [],
}

11 changes: 11 additions & 0 deletions examples/react/multi-file-upload/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
base: '',
plugins: [react()],
server: {
port: 3000,
}
})
20 changes: 20 additions & 0 deletions examples/react/uploads-list/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
24 changes: 24 additions & 0 deletions examples/react/uploads-list/.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?
8 changes: 8 additions & 0 deletions examples/react/uploads-list/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# React + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
Loading

0 comments on commit a924abf

Please sign in to comment.