diff --git a/apps/www/content/docs/media-placeholder.mdx b/apps/www/content/docs/media-placeholder.mdx index 9babde21a1..b2c3ec3651 100644 --- a/apps/www/content/docs/media-placeholder.mdx +++ b/apps/www/content/docs/media-placeholder.mdx @@ -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 --- @@ -22,6 +24,7 @@ npm install @udecode/plate-media ``` ## Usage +How to configuration the backend see [Upload](/docs/upload). ```tsx import { @@ -37,7 +40,14 @@ import { ```tsx const plugins = [ // ...otherPlugins, - PlaceholderPlugin, + PlaceholderPlugin.configure({ + options: { + disableEmptyPlaceholder: true, + }, + render: { + afterEditable: () => , + }, + }), ]; ``` @@ -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 + + ### Plate UI -Work in progress. +Refer to the preview above. ### Plate Plus - + ## Plugins diff --git a/apps/www/content/docs/upload.mdx b/apps/www/content/docs/upload.mdx index 750cf37bdd..c2932814bc 100644 --- a/apps/www/content/docs/upload.mdx +++ b/apps/www/content/docs/upload.mdx @@ -6,32 +6,27 @@ docs: title: Upload --- +### UploadThing Integration - +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 + + ### Plate UI Work in progress. ### Plate Plus - \ No newline at end of file + diff --git a/apps/www/public/r/styles/default/media-toolbar-button.json b/apps/www/public/r/styles/default/media-toolbar-button.json index 2961218f6e..d7f1d12617 100644 --- a/apps/www/public/r/styles/default/media-toolbar-button.json +++ b/apps/www/public/r/styles/default/media-toolbar-button.json @@ -17,7 +17,7 @@ }, "files": [ { - "content": "'use client';\n\nimport React, { useCallback, useEffect, useState } from 'react';\n\nimport type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';\n\nimport { cn } from '@udecode/cn';\nimport { useEditorRef } from '@udecode/plate-core/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n VideoPlugin,\n} from '@udecode/plate-media/react';\nimport { insertNodes } from '@udecode/slate';\nimport { focusEditor } from '@udecode/slate-react';\nimport {\n AudioLinesIcon,\n FileUpIcon,\n FilmIcon,\n ImageIcon,\n LinkIcon,\n} from 'lucide-react';\nimport { useFilePicker } from 'use-file-picker';\n\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from './alert-dialog';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuTrigger,\n useOpenState,\n} from './dropdown-menu';\nimport { Input } from './input';\nimport {\n ToolbarSplitButton,\n ToolbarSplitButtonPrimary,\n ToolbarSplitButtonSecondary,\n} from './toolbar';\nconst MEDIA_CONFIG: Record<\n string,\n {\n accept: string[];\n icon: React.ReactNode;\n title: string;\n tooltip: string;\n }\n> = {\n [AudioPlugin.key]: {\n accept: ['audio/*'],\n icon: ,\n title: 'Insert Audio',\n tooltip: 'Audio',\n },\n [FilePlugin.key]: {\n accept: ['*'],\n icon: ,\n title: 'Insert File',\n tooltip: 'File',\n },\n [ImagePlugin.key]: {\n accept: ['image/*'],\n icon: ,\n title: 'Insert Image',\n tooltip: 'Image',\n },\n [VideoPlugin.key]: {\n accept: ['video/*'],\n icon: ,\n title: 'Insert Video',\n tooltip: 'Video',\n },\n};\n\nexport function MediaToolbarButton({\n children,\n nodeType,\n ...props\n}: DropdownMenuProps & { nodeType: string }) {\n const currentConfig = MEDIA_CONFIG[nodeType];\n\n const editor = useEditorRef();\n const openState = useOpenState();\n\n const { openFilePicker } = useFilePicker({\n accept: currentConfig.accept,\n multiple: true,\n onFilesSelected: ({ plainFiles: updatedFiles }) => {\n (editor as any).tf.insert.media(updatedFiles);\n },\n });\n\n const [dialogOpen, setDialogOpen] = useState(false);\n\n const [url, setUrl] = useState('');\n\n const embedMedia = useCallback(() => {\n setDialogOpen(false);\n insertNodes(editor, {\n children: [{ text: '' }],\n name: nodeType === FilePlugin.key ? url.split('/').pop() : undefined,\n type: nodeType,\n url,\n });\n }, [editor, nodeType, url]);\n\n useEffect(() => {\n if (!dialogOpen) {\n focusEditor(editor);\n setUrl('');\n }\n }, [dialogOpen, editor]);\n\n return (\n <>\n \n \n openFilePicker()}\n onMouseDown={(e) => e.preventDefault()}\n tooltip={currentConfig.tooltip}\n >\n {currentConfig.icon}\n \n\n \n \n \n \n\n \n \n openFilePicker()}\n hideIcon\n >\n
\n {currentConfig.icon}\n Upload from computer\n
\n \n setDialogOpen(true)}\n hideIcon\n >\n
\n \n Insert via URL\n
\n \n
\n \n
\n\n \n \n \n {currentConfig.title}\n \n
\n \n URL\n \n setUrl(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') embedMedia();\n }}\n placeholder=\"\"\n type=\"email\"\n autoFocus\n />\n
\n
\n
\n \n Cancel\n Accept\n \n
\n
\n \n );\n}\n", + "content": "'use client';\n\nimport React, { useCallback, useState } from 'react';\n\nimport type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';\n\nimport { cn } from '@udecode/cn';\nimport { insertNodes, isUrl } from '@udecode/plate-common';\nimport { useEditorRef } from '@udecode/plate-core/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n VideoPlugin,\n} from '@udecode/plate-media/react';\nimport {\n AudioLinesIcon,\n FileUpIcon,\n FilmIcon,\n ImageIcon,\n LinkIcon,\n} from 'lucide-react';\nimport { toast } from 'sonner';\nimport { useFilePicker } from 'use-file-picker';\n\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from './alert-dialog';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuTrigger,\n useOpenState,\n} from './dropdown-menu';\nimport { Input } from './input';\nimport {\n ToolbarSplitButton,\n ToolbarSplitButtonPrimary,\n ToolbarSplitButtonSecondary,\n} from './toolbar';\nconst MEDIA_CONFIG: Record<\n string,\n {\n accept: string[];\n icon: React.ReactNode;\n title: string;\n tooltip: string;\n }\n> = {\n [AudioPlugin.key]: {\n accept: ['audio/*'],\n icon: ,\n title: 'Insert Audio',\n tooltip: 'Audio',\n },\n [FilePlugin.key]: {\n accept: ['*'],\n icon: ,\n title: 'Insert File',\n tooltip: 'File',\n },\n [ImagePlugin.key]: {\n accept: ['image/*'],\n icon: ,\n title: 'Insert Image',\n tooltip: 'Image',\n },\n [VideoPlugin.key]: {\n accept: ['video/*'],\n icon: ,\n title: 'Insert Video',\n tooltip: 'Video',\n },\n};\n\nexport function MediaToolbarButton({\n children,\n nodeType,\n ...props\n}: DropdownMenuProps & { nodeType: string }) {\n const currentConfig = MEDIA_CONFIG[nodeType];\n\n const editor = useEditorRef();\n const openState = useOpenState();\n\n const { openFilePicker } = useFilePicker({\n accept: currentConfig.accept,\n multiple: true,\n onFilesSelected: ({ plainFiles: updatedFiles }) => {\n (editor as any).tf.insert.media(updatedFiles);\n },\n });\n\n const [dialogOpen, setDialogOpen] = useState(false);\n\n const [url, setUrl] = useState('');\n\n const embedMedia = useCallback(() => {\n if (!isUrl(url)) return toast.error('Invalid URL');\n\n setDialogOpen(false);\n insertNodes(editor, {\n children: [{ text: '' }],\n name: nodeType === FilePlugin.key ? url.split('/').pop() : undefined,\n type: nodeType,\n url,\n });\n }, [url, editor, nodeType]);\n\n return (\n <>\n \n \n openFilePicker()}\n onMouseDown={(e) => e.preventDefault()}\n tooltip={currentConfig.tooltip}\n >\n {currentConfig.icon}\n \n\n \n \n \n \n\n \n \n openFilePicker()}\n hideIcon\n >\n
\n {currentConfig.icon}\n Upload from computer\n
\n \n setDialogOpen(true)}\n hideIcon\n >\n
\n \n Insert via URL\n
\n \n
\n \n
\n\n {\n setDialogOpen(value);\n setUrl('');\n }}\n >\n \n \n {currentConfig.title}\n \n \n URL\n \n setUrl(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') embedMedia();\n }}\n placeholder=\"\"\n type=\"email\"\n autoFocus\n />\n \n \n \n Cancel\n {\n e.preventDefault();\n embedMedia();\n }}\n >\n Accept\n \n \n \n \n \n );\n}\n", "path": "plate-ui/media-toolbar-button.tsx", "target": "components/plate-ui/media-toolbar-button.tsx", "type": "registry:ui" diff --git a/apps/www/src/config/customizer-components.ts b/apps/www/src/config/customizer-components.ts index d27d397794..e91e78ab44 100644 --- a/apps/www/src/config/customizer-components.ts +++ b/apps/www/src/config/customizer-components.ts @@ -220,6 +220,11 @@ export const customizerComponents = { label: 'Element', title: 'Media Embed', }, + mediaPlaceholderElement: { + href: '/docs/components/media-placeholder-element', + label: 'Element', + title: 'Media Placeholder', + }, mediaPopover: { href: '/docs/components/media-popover', title: 'Media Popover', @@ -228,6 +233,10 @@ export const customizerComponents = { href: '/docs/components/media-toolbar-button', title: 'Media Toolbar Button', }, + mediaUploadToast: { + href: '/docs/components/media-upload-toast', + title: 'Media Upload Toast', + }, mentionElement: { href: '/docs/components/mention-element', label: 'Element', diff --git a/apps/www/src/config/customizer-items.ts b/apps/www/src/config/customizer-items.ts index 7d95f55fdc..55befe2422 100644 --- a/apps/www/src/config/customizer-items.ts +++ b/apps/www/src/config/customizer-items.ts @@ -46,7 +46,11 @@ import { LineHeightPlugin } from '@udecode/plate-line-height/react'; import { LinkPlugin } from '@udecode/plate-link/react'; import { TodoListPlugin } from '@udecode/plate-list/react'; import { MarkdownPlugin } from '@udecode/plate-markdown'; -import { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react'; +import { + ImagePlugin, + MediaEmbedPlugin, + PlaceholderPlugin, +} from '@udecode/plate-media/react'; import { MentionInputPlugin, MentionPlugin, @@ -110,6 +114,14 @@ export type SettingPlugin = { }; export const customizerItems: Record = { + [`media_${PlaceholderPlugin.key}`]: { + id: `media_${PlaceholderPlugin.key}`, + label: 'MediaPlaceholder', + npmPackage: '@udecode/plate-placeholder', + pluginFactory: 'PlaceholderPlugin', + reactImport: true, + route: customizerPlugins.mediaPlaceholder.route, + }, [AIChatPlugin.key]: { id: AIChatPlugin.key, badges: [customizerBadges.handler], diff --git a/apps/www/src/config/customizer-list.ts b/apps/www/src/config/customizer-list.ts index 57a4c9844c..a266b2d081 100644 --- a/apps/www/src/config/customizer-list.ts +++ b/apps/www/src/config/customizer-list.ts @@ -42,7 +42,11 @@ import { LineHeightPlugin } from '@udecode/plate-line-height/react'; import { LinkPlugin } from '@udecode/plate-link/react'; import { TodoListPlugin } from '@udecode/plate-list/react'; import { MarkdownPlugin } from '@udecode/plate-markdown'; -import { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react'; +import { + ImagePlugin, + MediaEmbedPlugin, + PlaceholderPlugin, +} from '@udecode/plate-media/react'; import { MentionPlugin } from '@udecode/plate-mention/react'; import { NodeIdPlugin } from '@udecode/plate-node-id'; import { NormalizeTypesPlugin } from '@udecode/plate-normalizers'; @@ -88,6 +92,7 @@ export const customizerList = [ customizerItems.heading, customizerItems.list, customizerItems[MediaEmbedPlugin.key], + customizerItems[`media_${PlaceholderPlugin.key}`], customizerItems[MentionPlugin.key], customizerItems[ParagraphPlugin.key], customizerItems[TablePlugin.key], @@ -175,6 +180,8 @@ export const orderedPluginKeys = [ 'list', ImagePlugin.key, MediaEmbedPlugin.key, + PlaceholderPlugin.key, + CaptionPlugin.key, MentionPlugin.key, TablePlugin.key, diff --git a/apps/www/src/config/customizer-plugins.ts b/apps/www/src/config/customizer-plugins.ts index 1f12dbb832..a8fb320d15 100644 --- a/apps/www/src/config/customizer-plugins.ts +++ b/apps/www/src/config/customizer-plugins.ts @@ -78,7 +78,10 @@ import { kbdValue } from '@/plate/demo/values/kbdValue'; import { lineHeightValue } from '@/plate/demo/values/lineHeightValue'; import { linkValue } from '@/plate/demo/values/linkValue'; import { listValue, todoListValue } from '@/plate/demo/values/listValue'; -import { mediaValue } from '@/plate/demo/values/mediaValue'; +import { + mediaPlaceholderValue, + mediaValue, +} from '@/plate/demo/values/mediaValue'; import { mentionValue } from '@/plate/demo/values/mentionValue'; import { placeholderValue } from '@/plate/demo/values/placeholderValue'; import { singleLineValue } from '@/plate/demo/values/singleLineValue'; @@ -343,6 +346,12 @@ export const customizerPlugins = { route: '/docs/media', value: mediaValue, }, + mediaPlaceholder: { + id: 'media-placeholder', + label: 'MediaPlaceholder', + route: '/docs/media-placeholder', + value: mediaPlaceholderValue, + }, mention: { id: 'mention', label: 'Mention', diff --git a/apps/www/src/config/descriptions.ts b/apps/www/src/config/descriptions.ts index 395aea6c17..8096f934f8 100644 --- a/apps/www/src/config/descriptions.ts +++ b/apps/www/src/config/descriptions.ts @@ -40,7 +40,11 @@ import { LineHeightPlugin } from '@udecode/plate-line-height/react'; import { LinkPlugin } from '@udecode/plate-link/react'; import { TodoListPlugin } from '@udecode/plate-list/react'; import { MarkdownPlugin } from '@udecode/plate-markdown'; -import { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react'; +import { + ImagePlugin, + MediaEmbedPlugin, + PlaceholderPlugin, +} from '@udecode/plate-media/react'; import { MentionPlugin } from '@udecode/plate-mention/react'; import { NodeIdPlugin } from '@udecode/plate-node-id'; import { NormalizeTypesPlugin } from '@udecode/plate-normalizers'; @@ -61,6 +65,7 @@ import { FixedToolbarPlugin } from '@/registry/default/components/editor/plugins import { FloatingToolbarPlugin } from '@/registry/default/components/editor/plugins/floating-toolbar-plugin'; export const descriptions: Record = { + [`media_${PlaceholderPlugin.key}`]: 'Add placeholder to your media blocks.', [AIChatPlugin.key]: 'AI menu with commands, streaming responses in a preview or directly into the editor.', [AIPlugin.key]: 'AI transforms.', diff --git a/apps/www/src/lib/plate/demo/values/mediaValue.tsx b/apps/www/src/lib/plate/demo/values/mediaValue.tsx index 6ab7bdb62c..d95f82cb85 100644 --- a/apps/www/src/lib/plate/demo/values/mediaValue.tsx +++ b/apps/www/src/lib/plate/demo/values/mediaValue.tsx @@ -22,16 +22,40 @@ export const imageValue: any = ( export const mediaPlaceholderValue: any = ( Upload - - Easily upload media files by dragging and dropping them into the editor or - using the file picker. The editor provides: + + Our editor supports various media types for upload, including images, + videos, audio, and files. + + + Real-time upload status and progress tracking + + + Configurable file size limits and batch upload settings + + + Clear error messages for any upload issues diff --git a/apps/www/src/registry/default/plate-ui/media-upload-toast.tsx b/apps/www/src/registry/default/plate-ui/media-upload-toast.tsx index ab899deff9..ce39cb2e79 100644 --- a/apps/www/src/registry/default/plate-ui/media-upload-toast.tsx +++ b/apps/www/src/registry/default/plate-ui/media-upload-toast.tsx @@ -13,7 +13,6 @@ export const useUploadErrorToast = () => { if (!uploadError) return; const { code, data } = uploadError; - console.log('🚀 ~ useEffect ~ data:', data); switch (code) { case UploadErrorCode.INVALID_FILE_SIZE: { diff --git a/apps/www/src/registry/registry-examples.ts b/apps/www/src/registry/registry-examples.ts index 3b82b51bfd..102e1c65ca 100644 --- a/apps/www/src/registry/registry-examples.ts +++ b/apps/www/src/registry/registry-examples.ts @@ -29,12 +29,14 @@ export const proExamples: Registry = [ - Support for various media types: images, videos, audio, and files - Use slash commands for quick insertion - Image-specific features: + - **Better loading animation and image replacement** - Resize using vertical edge bars - Alignment options - Caption support - Expand/collapse view - Easy download - Video-specific features: + - Lazy load - Resize using vertical edge bars - Alignment options - Caption support diff --git a/apps/www/src/registry/registry-ui.ts b/apps/www/src/registry/registry-ui.ts index 83530d89bd..898685c03b 100644 --- a/apps/www/src/registry/registry-ui.ts +++ b/apps/www/src/registry/registry-ui.ts @@ -844,6 +844,40 @@ export const uiNodes: Registry = [ registryDependencies: [], type: 'registry:ui', }, + { + dependencies: [ + '@udecode/plate-media', + 'use-file-picker', + '@uploadthing/react@7.1.0', + 'uploadthing@7.2.0', + 'zod', + 'sonner', + ], + doc: { + description: 'A placeholder for media files.', + docs: [{ route: '/docs/media-placeholder', title: 'Media Placeholder' }], + examples: ['media-demo', 'media-toolbar-pro'], + }, + files: [ + '../../../../../templates/plate-playground-template/src/components/plate-ui/media-placeholder-element.tsx', + '../../../../../templates/plate-playground-template/src/lib/uploadthing/uploadthing.ts', + ], + 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: ['plate-ui/media-upload-toast.tsx'], + name: 'media-upload-toast', + registryDependencies: [], + type: 'registry:ui', + }, { dependencies: [], doc: { diff --git a/templates/plate-playground-template/src/components/plate-ui/media-placeholder-element.tsx b/templates/plate-playground-template/src/components/plate-ui/media-placeholder-element.tsx index 8f2eb722d9..76713b8be0 100644 --- a/templates/plate-playground-template/src/components/plate-ui/media-placeholder-element.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/media-placeholder-element.tsx @@ -230,6 +230,7 @@ export function ImageProgress({ return (
+ {/* eslint-disable-next-line @next/next/no-img-element */}
-
+
diff --git a/templates/plate-playground-template/src/lib/uploadthing/handle-error.ts b/templates/plate-playground-template/src/lib/uploadthing/handle-error.ts deleted file mode 100644 index f04db185ad..0000000000 --- a/templates/plate-playground-template/src/lib/uploadthing/handle-error.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { isRedirectError } from 'next/dist/client/components/redirect'; -import { toast } from 'sonner'; -import { z } from 'zod'; - -export function getErrorMessage(err: unknown) { - const unknownError = 'Something went wrong, please try again later.'; - - if (err instanceof z.ZodError) { - const errors = err.issues.map((issue) => { - return issue.message; - }); - - return errors.join('\n'); - } else if (err instanceof Error) { - return err.message; - } else if (isRedirectError(err)) { - throw err; - } else { - return unknownError; - } -} - -export function showErrorToast(err: unknown) { - const errorMessage = getErrorMessage(err); - - return toast.error(errorMessage); -} diff --git a/templates/plate-playground-template/src/lib/uploadthing/index.ts b/templates/plate-playground-template/src/lib/uploadthing/index.ts index 55dcc822a3..d83b377c7d 100644 --- a/templates/plate-playground-template/src/lib/uploadthing/index.ts +++ b/templates/plate-playground-template/src/lib/uploadthing/index.ts @@ -1,5 +1 @@ -export * from './handle-error'; - export * from './uploadthing'; - -export * from './use-upload-file'; diff --git a/templates/plate-playground-template/src/lib/uploadthing/uploadthing.ts b/templates/plate-playground-template/src/lib/uploadthing/uploadthing.ts index 90f48864d6..c5dbe904ce 100644 --- a/templates/plate-playground-template/src/lib/uploadthing/uploadthing.ts +++ b/templates/plate-playground-template/src/lib/uploadthing/uploadthing.ts @@ -1,6 +1,102 @@ +import * as React from 'react'; + import type { OurFileRouter } from '@/app/api/uploadthing/core'; +import type { + ClientUploadedFileData, + UploadFilesOptions, +} from 'uploadthing/types'; import { generateReactHelpers } from '@uploadthing/react'; +import { isRedirectError } from 'next/dist/client/components/redirect'; +import { toast } from 'sonner'; +import { z } from 'zod'; + +export interface UploadedFile extends ClientUploadedFileData {} + +interface UseUploadFileProps + extends Pick< + UploadFilesOptions, + 'headers' | 'onUploadBegin' | 'onUploadProgress' | 'skipPolling' + > { + onUploadComplete?: (file: UploadedFile) => void; + onUploadError?: (error: unknown) => void; +} + +export function useUploadFile( + endpoint: keyof OurFileRouter, + { onUploadComplete, onUploadError, ...props }: UseUploadFileProps = {} +) { + const [uploadedFile, setUploadedFile] = React.useState(); + const [uploadingFile, setUploadingFile] = React.useState(); + const [progress, setProgress] = React.useState(0); + const [isUploading, setIsUploading] = React.useState(false); + + async function uploadThing(file: File) { + setIsUploading(true); + setUploadingFile(file); + + try { + const res = await uploadFiles(endpoint, { + ...props, + files: [file], + onUploadProgress: ({ progress }) => { + setProgress(Math.min(progress, 100)); + }, + }); + + setUploadedFile(res[0]); + + onUploadComplete?.(res[0]); + return uploadedFile; + } catch (error) { + const errorMessage = getErrorMessage(error); + + const message = + errorMessage.length > 0 + ? errorMessage + : 'Something went wrong, please try again later.'; + + toast.error(message); + onUploadError?.(error); + } finally { + setProgress(0); + setIsUploading(false); + setUploadingFile(undefined); + } + } + + return { + isUploading, + progress, + uploadFile: uploadThing, + uploadedFile, + uploadingFile, + }; +} export const { uploadFiles, useUploadThing } = generateReactHelpers(); + +export function getErrorMessage(err: unknown) { + const unknownError = 'Something went wrong, please try again later.'; + + if (err instanceof z.ZodError) { + const errors = err.issues.map((issue) => { + return issue.message; + }); + + return errors.join('\n'); + } else if (err instanceof Error) { + return err.message; + } else if (isRedirectError(err)) { + throw err; + } else { + return unknownError; + } +} + +export function showErrorToast(err: unknown) { + const errorMessage = getErrorMessage(err); + + return toast.error(errorMessage); +} diff --git a/templates/plate-playground-template/src/lib/uploadthing/use-upload-file.ts b/templates/plate-playground-template/src/lib/uploadthing/use-upload-file.ts deleted file mode 100644 index d4d62b6aef..0000000000 --- a/templates/plate-playground-template/src/lib/uploadthing/use-upload-file.ts +++ /dev/null @@ -1,76 +0,0 @@ -import * as React from 'react'; - -import type { OurFileRouter } from '@/app/api/uploadthing/core'; -import type { - ClientUploadedFileData, - UploadFilesOptions, -} from 'uploadthing/types'; - -import { toast } from 'sonner'; - -import { getErrorMessage } from './handle-error'; -import { uploadFiles } from './uploadthing'; - -export interface UploadedFile extends ClientUploadedFileData {} - -interface UseUploadFileProps - extends Pick< - UploadFilesOptions, - 'headers' | 'onUploadBegin' | 'onUploadProgress' | 'skipPolling' - > { - onUploadComplete?: (file: UploadedFile) => void; - onUploadError?: (error: unknown) => void; -} - -export function useUploadFile( - endpoint: keyof OurFileRouter, - { onUploadComplete, onUploadError, ...props }: UseUploadFileProps = {} -) { - const [uploadedFile, setUploadedFile] = React.useState(); - const [uploadingFile, setUploadingFile] = React.useState(); - const [progress, setProgress] = React.useState(0); - const [isUploading, setIsUploading] = React.useState(false); - - async function uploadThing(file: File) { - setIsUploading(true); - setUploadingFile(file); - - try { - const res = await uploadFiles(endpoint, { - ...props, - files: [file], - onUploadProgress: ({ progress }) => { - setProgress(Math.min(progress, 100)); - }, - }); - - setUploadedFile(res[0]); - - onUploadComplete?.(res[0]); - - return uploadedFile; - } catch (error) { - const errorMessage = getErrorMessage(error); - - const message = - errorMessage.length > 0 - ? errorMessage - : 'Something went wrong, please try again later.'; - - toast.error(message); - onUploadError?.(error); - } finally { - setProgress(0); - setIsUploading(false); - setUploadingFile(undefined); - } - } - - return { - isUploading, - progress, - uploadFile: uploadThing, - uploadedFile, - uploadingFile, - }; -}