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

Docs/upload #3782

Merged
merged 6 commits into from
Nov 18, 2024
Merged
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
5 changes: 5 additions & 0 deletions .changeset/thin-dragons-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-media': patch
---

Fix error message.
3 changes: 2 additions & 1 deletion apps/www/content/docs/components/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ Use the [CLI](https://platejs.org/docs/components/cli) to install the latest ver

### November 14 #16.7

Add `ToolbarSplitButton` in `toolbar.tsx`.
Add `ToolbarSplitButton`, `ToolbarSplitButtonPrimary`, `ToolbarSplitButtonSecondary` in `toolbar.tsx`.
Refactor `media-toolbar-button.tsx` to use the new split button.

### November 13 #16.6

Expand Down
114 changes: 109 additions & 5 deletions apps/www/content/docs/media-placeholder.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
title: Media Placeholder
description: Media placeholders to be used as clickable placeholders for various media types (image, video, audio, file).
docs:
- route: https://pro.platejs.org/docs/components/media-placeholder-element
- route: components/media-placeholder-element
title: Media Placeholder Element
- route: components/media-upload-toast
title: Media Upload Toast
---

<PackageInfo>
Expand All @@ -22,6 +24,7 @@ npm install @udecode/plate-media
```

## Usage
How to configuration the backend see [Upload](/docs/upload).

```tsx
import {
Expand All @@ -37,7 +40,14 @@ import {
```tsx
const plugins = [
// ...otherPlugins,
PlaceholderPlugin,
PlaceholderPlugin.configure({
options: {
disableEmptyPlaceholder: true,
},
render: {
afterEditable: () => <MediaUploadToast />,
},
}),
];
```

Expand All @@ -48,17 +58,111 @@ const components = {
};
```

- [MediaPlaceholderElement](https://pro.platejs.org/docs/components/media-placeholder-element) (Plus)

## UploadOptions

### uploadConfig

Configuration for different file types:

- You can use this option to configure upload limits for each file type, including:

- Maximum file count (e.g. `maxFileCount: 1`)
- Maximum file size (e.g. `maxFileSize: '8MB'`)
- Minimum file count (e.g. `minFileCount: 1`)
- mediaType: Used for passing to the media-placeholder-elements file to distinguish between different file types and their progress bar styles.

default configuration:

```tsx
uploadConfig: {
audio: {
maxFileCount: 1,
maxFileSize: '8MB',
mediaType: AudioPlugin.key,
minFileCount: 1,
},
blob: {
maxFileCount: 1,
maxFileSize: '8MB',
mediaType: FilePlugin.key,
minFileCount: 1,
},
image: {
maxFileCount: 3,
maxFileSize: '4MB',
mediaType: ImagePlugin.key,
minFileCount: 1,
},
pdf: {
maxFileCount: 1,
maxFileSize: '4MB',
mediaType: FilePlugin.key,
minFileCount: 1,
},
text: {
maxFileCount: 1,
maxFileSize: '64KB',
mediaType: FilePlugin.key,
minFileCount: 1,
},
video: {
maxFileCount: 1,
maxFileSize: '16MB',
mediaType: VideoPlugin.key,
minFileCount: 1,
},
},
```

here is all allowed file types (keys for `uploadConfig`):

```tsx
export const ALLOWED_FILE_TYPES = [
'image',
'video',
'audio',
'pdf',
'text',
'blob',
] as const;
```

### disableEmptyPlaceholder

`boolean` (default: `false`)

Disable empty placeholder when no file is selected.

### disableFileDrop

`boolean` (default: `false`)

Whether we can undo to the placeholder after the file upload is complete.

### maxFileCount

`number` (default: `5`)

Maximum number of files that can be uploaded at once.

### multiple

`boolean` (default: `true`)

Whether multiple files can be uploaded in one time.

## Examples

<ComponentPreview name="playground-demo" id="mediaPlaceholder" />

### Plate UI

Work in progress.
Refer to the preview above.

### Plate Plus

<ComponentPreviewPro name="media-placeholder-pro" />
<ComponentPreviewPro name="upload-pro" />

## Plugins

Expand Down
23 changes: 9 additions & 14 deletions apps/www/content/docs/upload.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,27 @@ docs:
title: Upload
---

### UploadThing Integration

<ComponentPreview name="playground-demo" id="upload" />
Make sure you have install the [media-placeholder-element](/docs/components/media-placeholder-element) component and all the dependencies.

{/* ### UploadThing Integration
Set `UPLOADTHING_TOKEN` in your .env file [get one here](https://uploadthing.com/dashboard).

This component uses UploadThing for file uploads. UploadThing provides a simple and efficient way to handle file uploads in your application.

To use UploadThing:
### Using your own backend

1. Set up an UploadThing account and configure your upload endpoints.
2. Install the UploadThing client in your project:
Remove this two folder `lib/uploadthing` and `/api/uploadthing`.

```bash
npm install uploadthing
```
3. Configure the UploadThing client in your application.
For more details on setting up UploadThing, refer to their [documentation](https://docs.uploadthing.com/). */}
Then impelement a similar hooks like `useUploadFile` using your own backend.

## Examples

<ComponentPreview name="playground-demo" id="mediaPlaceholder" />

### Plate UI

Work in progress.

### Plate Plus

<ComponentPreviewPro name="upload-pro" />
<ComponentPreviewPro name="upload-pro" />
61 changes: 61 additions & 0 deletions apps/www/public/r/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,67 @@
"registryDependencies": [],
"type": "registry:ui"
},
{
"dependencies": [
"@udecode/plate-media",
"use-file-picker",
"@uploadthing/[email protected]",
"[email protected]",
"zod",
"sonner"
],
"doc": {
"description": "A placeholder for media files.",
"docs": [
{
"route": "/docs/media-placeholder",
"title": "Media Placeholder"
}
],
"examples": [
"media-demo",
"media-toolbar-pro"
]
},
"files": [
{
"path": "plate-ui/media-placeholder-element.tsx",
"type": "registry:ui"
},
{
"path": "lib/uploadthing/uploadthing.ts",
"type": "registry:ui"
}
],
"name": "media-placeholder-element",
"registryDependencies": [],
"type": "registry:ui"
},
{
"dependencies": [],
"doc": {
"description": "A toast for media uploads.",
"docs": [
{
"route": "/docs/media-placeholder",
"title": "Media Placeholder"
}
],
"examples": [
"media-demo",
"upload-pro"
]
},
"files": [
{
"path": "plate-ui/media-upload-toast.tsx",
"type": "registry:ui"
}
],
"name": "media-upload-toast",
"registryDependencies": [],
"type": "registry:ui"
},
{
"dependencies": [],
"doc": {
Expand Down
40 changes: 40 additions & 0 deletions apps/www/public/r/styles/default/media-placeholder-element.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"dependencies": [
"@udecode/plate-media",
"use-file-picker",
"@uploadthing/[email protected]",
"[email protected]",
"zod",
"sonner"
],
"doc": {
"description": "A placeholder for media files.",
"docs": [
{
"route": "/docs/media-placeholder",
"title": "Media Placeholder"
}
],
"examples": [
"media-demo",
"media-toolbar-pro"
]
},
"files": [
{
"content": "'use client';\n\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { TPlaceholderElement } from '@udecode/plate-media';\n\nimport { cn } from '@udecode/cn';\nimport {\n insertNodes,\n removeNodes,\n withoutSavingHistory,\n} from '@udecode/plate-common';\nimport {\n findNodePath,\n useEditorPlugin,\n withHOC,\n withRef,\n} from '@udecode/plate-common/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n PlaceholderPlugin,\n PlaceholderProvider,\n VideoPlugin,\n updateUploadHistory,\n} from '@udecode/plate-media/react';\nimport { AudioLines, FileUp, Film, ImageIcon } from 'lucide-react';\nimport { useFilePicker } from 'use-file-picker';\n\nimport { useUploadFile } from '../lib/uploadthing';\nimport { PlateElement } from './plate-element';\nimport { Spinner } from './spinner';\n\nconst CONTENT: Record<\n string,\n {\n accept: string[];\n content: ReactNode;\n icon: ReactNode;\n }\n> = {\n [AudioPlugin.key]: {\n accept: ['audio/*'],\n content: 'Add an audio file',\n icon: <AudioLines />,\n },\n [FilePlugin.key]: {\n accept: ['*'],\n content: 'Add a file',\n icon: <FileUp />,\n },\n [ImagePlugin.key]: {\n accept: ['image/*'],\n content: 'Add an image',\n icon: <ImageIcon />,\n },\n [VideoPlugin.key]: {\n accept: ['video/*'],\n content: 'Add a video',\n icon: <Film />,\n },\n};\n\nexport const MediaPlaceholderElement = withHOC(\n PlaceholderProvider,\n withRef<typeof PlateElement>(\n ({ children, className, editor, nodeProps, ...props }, ref) => {\n const element = props.element as TPlaceholderElement;\n\n const { api } = useEditorPlugin(PlaceholderPlugin);\n\n const { isUploading, progress, uploadFile, uploadedFile, uploadingFile } =\n useUploadFile();\n\n const loading = isUploading && uploadingFile;\n\n const currentContent = CONTENT[element.mediaType];\n\n const isImage = element.mediaType === ImagePlugin.key;\n\n const imageRef = useRef<HTMLImageElement>(null);\n\n const { openFilePicker } = useFilePicker({\n accept: currentContent.accept,\n multiple: true,\n onFilesSelected: ({ plainFiles: updatedFiles }) => {\n const firstFile = updatedFiles[0];\n const restFiles = updatedFiles.slice(1);\n\n replaceCurrentPlaceholder(firstFile);\n\n restFiles.length > 0 && (editor as any).tf.insert.media(restFiles);\n },\n });\n\n const replaceCurrentPlaceholder = useCallback(\n (file: File) => {\n void uploadFile(file);\n api.placeholder.addUploadingFile(element.id as string, file);\n },\n [api.placeholder, element.id, uploadFile]\n );\n\n useEffect(() => {\n if (!uploadedFile) return;\n\n const path = findNodePath(editor, element);\n\n withoutSavingHistory(editor, () => {\n removeNodes(editor, { at: path });\n\n const node = {\n children: [{ text: '' }],\n initialHeight: imageRef.current?.height,\n initialWidth: imageRef.current?.width,\n isUpload: true,\n name: element.mediaType === FilePlugin.key ? uploadedFile.name : '',\n placeholderId: element.id as string,\n type: element.mediaType!,\n url: uploadedFile.url,\n };\n\n insertNodes(editor, node, { at: path });\n\n updateUploadHistory(editor, node);\n });\n\n api.placeholder.removeUploadingFile(element.id as string);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [uploadedFile, element.id]);\n\n // React dev mode will call useEffect twice\n const isReplaced = useRef(false);\n /** Paste and drop */\n useEffect(() => {\n if (isReplaced.current) return;\n\n isReplaced.current = true;\n const currentFiles = api.placeholder.getUploadingFile(\n element.id as string\n );\n\n if (!currentFiles) return;\n\n replaceCurrentPlaceholder(currentFiles);\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isReplaced]);\n\n return (\n <PlateElement\n ref={ref}\n className={cn('relative my-1', className)}\n editor={editor}\n {...props}\n >\n {(!loading || !isImage) && (\n <div\n className={cn(\n 'flex cursor-pointer select-none items-center rounded-sm bg-muted p-3 pr-9 hover:bg-primary/10'\n )}\n onClick={() => !loading && openFilePicker()}\n contentEditable={false}\n >\n <div className=\"relative mr-3 flex text-muted-foreground/80 [&_svg]:size-6\">\n {currentContent.icon}\n </div>\n <div className=\"whitespace-nowrap text-sm text-muted-foreground\">\n <div>\n {loading ? uploadingFile?.name : currentContent.content}\n </div>\n\n {loading && !isImage && (\n <div className=\"mt-1 flex items-center gap-1.5\">\n <div>{formatBytes(uploadingFile?.size ?? 0)}</div>\n <div>–</div>\n <div className=\"flex items-center\">\n <Spinner className=\"mr-1 size-3.5\" />\n {progress ?? 0}%\n </div>\n </div>\n )}\n </div>\n </div>\n )}\n\n {isImage && loading && (\n <ImageProgress\n file={uploadingFile}\n imageRef={imageRef}\n progress={progress}\n />\n )}\n\n {children}\n </PlateElement>\n );\n }\n )\n);\n\nexport function ImageProgress({\n className,\n file,\n imageRef,\n progress = 0,\n}: {\n file: File;\n className?: string;\n imageRef?: React.RefObject<HTMLImageElement>;\n progress?: number;\n}) {\n const [objectUrl, setObjectUrl] = useState<string | null>(null);\n\n useEffect(() => {\n const url = URL.createObjectURL(file);\n setObjectUrl(url);\n\n return () => {\n URL.revokeObjectURL(url);\n };\n }, [file]);\n\n if (!objectUrl) {\n return null;\n }\n\n return (\n <div className={cn('relative', className)} contentEditable={false}>\n <img\n ref={imageRef}\n className=\"h-auto w-full rounded-sm object-cover\"\n alt={file.name}\n src={objectUrl}\n />\n {progress < 100 && (\n <div className=\"absolute bottom-1 right-1 flex items-center space-x-2 rounded-full bg-black/50 px-1 py-0.5\">\n <Spinner />\n <span className=\"text-xs font-medium text-white\">\n {Math.round(progress)}%\n </span>\n </div>\n )}\n </div>\n );\n}\n\nexport function formatBytes(\n bytes: number,\n opts: {\n decimals?: number;\n sizeType?: 'accurate' | 'normal';\n } = {}\n) {\n const { decimals = 0, sizeType = 'normal' } = opts;\n\n const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];\n const accurateSizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'];\n\n if (bytes === 0) return '0 Byte';\n\n const i = Math.floor(Math.log(bytes) / Math.log(1024));\n\n return `${(bytes / Math.pow(1024, i)).toFixed(decimals)} ${\n sizeType === 'accurate'\n ? (accurateSizes[i] ?? 'Bytest')\n : (sizes[i] ?? 'Bytes')\n }`;\n}\n",
"path": "plate-ui/media-placeholder-element.tsx",
"target": "components/plate-ui/media-placeholder-element.tsx",
"type": "registry:ui"
},
{
"content": "import * as React from 'react';\n\nimport { isRedirectError } from 'next/dist/client/components/redirect';\nimport { toast } from 'sonner';\nimport { z } from 'zod';\n\nexport interface UploadedFile {\n key: string;\n appUrl: string;\n name: string;\n size: number;\n type: string;\n url: string;\n}\n\nexport function useUploadFile() {\n const [uploadedFile, setUploadedFile] = React.useState<UploadedFile>();\n const [uploadingFile, setUploadingFile] = React.useState<File>();\n const [progress, setProgress] = React.useState<number>(0);\n const [isUploading, setIsUploading] = React.useState(false);\n\n async function uploadThing(file: File) {\n setIsUploading(true);\n setUploadingFile(file);\n\n try {\n // Mock upload for unauthenticated users\n // toast.info('User not logged in. Mocking upload process.');\n const mockUploadedFile = {\n key: 'mock-key-0',\n appUrl: `https://mock-app-url.com/${file.name}`,\n name: file.name,\n size: file.size,\n type: file.type,\n url: URL.createObjectURL(file),\n } as UploadedFile;\n\n // Simulate upload progress\n let progress = 0;\n\n const simulateProgress = async () => {\n while (progress < 100) {\n await new Promise((resolve) => setTimeout(resolve, 100));\n progress += 2;\n setProgress(Math.min(progress, 100));\n }\n };\n\n await simulateProgress();\n\n setUploadedFile(mockUploadedFile);\n\n return mockUploadedFile;\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n\n const message =\n errorMessage.length > 0\n ? errorMessage\n : 'Something went wrong, please try again later.';\n\n toast.error(message);\n } finally {\n setProgress(0);\n setIsUploading(false);\n setUploadingFile(undefined);\n }\n }\n\n return {\n isUploading,\n progress,\n uploadFile: uploadThing,\n uploadedFile,\n uploadingFile,\n };\n}\n\nexport function getErrorMessage(err: unknown) {\n const unknownError = 'Something went wrong, please try again later.';\n\n if (err instanceof z.ZodError) {\n const errors = err.issues.map((issue) => {\n return issue.message;\n });\n\n return errors.join('\\n');\n } else if (err instanceof Error) {\n return err.message;\n } else if (isRedirectError(err)) {\n throw err;\n } else {\n return unknownError;\n }\n}\n\nexport function showErrorToast(err: unknown) {\n const errorMessage = getErrorMessage(err);\n\n return toast.error(errorMessage);\n}\n",
"path": "lib/uploadthing/uploadthing.ts",
"target": "components/plate-ui/uploadthing.ts",
"type": "registry:ui"
}
],
"name": "media-placeholder-element",
"registryDependencies": [],
"type": "registry:ui"
}
Loading
Loading