From 01dcf2535b884cbc7e45185b2fd56789b0026a2f Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Tue, 19 Nov 2024 09:32:06 +0800 Subject: [PATCH 1/9] fix --- apps/www/content/docs/media-placeholder.mdx | 72 ++++++++++--------- apps/www/content/docs/upload.mdx | 49 +++++++++++-- .../react/placeholder/PlaceholderPlugin.tsx | 4 +- .../placeholder/transforms/insertMedia.ts | 5 +- .../src/react/placeholder/utils/history.ts | 3 +- 5 files changed, 90 insertions(+), 43 deletions(-) diff --git a/apps/www/content/docs/media-placeholder.mdx b/apps/www/content/docs/media-placeholder.mdx index b2c3ec3651..1917097356 100644 --- a/apps/www/content/docs/media-placeholder.mdx +++ b/apps/www/content/docs/media-placeholder.mdx @@ -59,10 +59,33 @@ const components = { ``` -## UploadOptions -### uploadConfig +### multiple + +`boolean` (default: `true`) + +Whether multiple files can be uploaded in one time. + +## Examples + + + +### Plate UI + +Refer to the preview above. +### Plate Plus + + + +## Plugins + +### PlaceholderPlugin + +Media placeholder element plugin. + + + Configuration for different file types: - You can use this option to configure upload limits for each file type, including: @@ -73,7 +96,6 @@ Configuration for different file types: - 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: { @@ -115,6 +137,7 @@ Configuration for different file types: }, ``` + here is all allowed file types (keys for `uploadConfig`): ```tsx @@ -128,47 +151,26 @@ Configuration for different file types: ] as const; ``` -### disableEmptyPlaceholder - -`boolean` (default: `false`) + + Disable empty placeholder when no file is selected. -### disableFileDrop - -`boolean` (default: `false`) +- **Default:** `false` + + Whether we can undo to the placeholder after the file upload is complete. -### maxFileCount - -`number` (default: `5`) +- **Default:** `false` + + 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 - -Refer to the preview above. - -### Plate Plus - - - -## Plugins - -### PlaceholderPlugin - -Media placeholder element plugin. +- **Default:** `5` + + ## Transforms diff --git a/apps/www/content/docs/upload.mdx b/apps/www/content/docs/upload.mdx index c2932814bc..a5c26a71d6 100644 --- a/apps/www/content/docs/upload.mdx +++ b/apps/www/content/docs/upload.mdx @@ -8,16 +8,57 @@ docs: ### UploadThing Integration -Make sure you have install the [media-placeholder-element](/docs/components/media-placeholder-element) component and all the dependencies. +The UploadThing integration provides an easy way to handle file uploads in your editor. Follow these steps to set it up: -Set `UPLOADTHING_TOKEN` in your .env file [get one here](https://uploadthing.com/dashboard). +1. Install the required dependencies: + ```bash + npm install @uploadthing/react uploadthing + ``` + +2. Install the [media-placeholder-element](/docs/components/media-placeholder-element) component. + +3. Set up the UploadThing API route by copying the [example implementation](https://github.com/udecode/plate/blob/main/templates/plate-playground-template/src/app/api/uploadthing/core.ts). + +4. Get your UploadThing API key from the [dashboard](https://uploadthing.com/dashboard) and add it to your `.env` file: + ```bash + UPLOADTHING_SECRET=your_secret_key + ``` ### Using your own backend -Remove this two folder `lib/uploadthing` and `/api/uploadthing`. +To implement your own backend for file uploads: + +1. Remove the UploadThing implementation: + ```bash + lib/uploadthing/ + api/uploadthing/ + ``` + +2. Create your own upload hook: + ```ts + function useUploadFile() { + // Your implementation here + return { + isUploading: boolean, + progress: number, + uploadFile: (file: File) => Promise, + uploadedFile: UploadedFile | undefined, + uploadingFile: File | undefined + } + } + ``` -Then impelement a similar hooks like `useUploadFile` using your own backend. +3. The hook should match the interface expected by the media placeholder component: + ```ts + interface UploadedFile { + key: string + url: string + name: string + size: number + type: string + } + ``` ## Examples diff --git a/packages/media/src/react/placeholder/PlaceholderPlugin.tsx b/packages/media/src/react/placeholder/PlaceholderPlugin.tsx index b05c1ed48f..db4d6c4c18 100644 --- a/packages/media/src/react/placeholder/PlaceholderPlugin.tsx +++ b/packages/media/src/react/placeholder/PlaceholderPlugin.tsx @@ -180,12 +180,12 @@ export const PlaceholderPlugin = toTPlatePlugin< if (getNodeString(node).length === 0) { removeNodes(editor, { at: path }); - tf.insert.media(files, { at: path }); + tf.insert.media(files, { at: path, nextBlock: false }); inserted = true; } } if (!inserted) { - tf.insert.media(files); + tf.insert.media(files, { nextBlock: false }); } return true; diff --git a/packages/media/src/react/placeholder/transforms/insertMedia.ts b/packages/media/src/react/placeholder/transforms/insertMedia.ts index 9441de17f9..42afe7dfa3 100644 --- a/packages/media/src/react/placeholder/transforms/insertMedia.ts +++ b/packages/media/src/react/placeholder/transforms/insertMedia.ts @@ -4,6 +4,7 @@ import { type InsertNodesOptions, insertNodes, nanoid, + withoutMergingHistory, withoutNormalizing, } from '@udecode/plate-common'; import { Path } from 'slate'; @@ -94,7 +95,9 @@ export const insertMedia = ( ); if (disableEmptyPlaceholder) { - withHistoryMark(editor, insert); + withoutMergingHistory(editor, () => { + withHistoryMark(editor, insert); + }); } else { withoutNormalizing(editor, insert); } diff --git a/packages/media/src/react/placeholder/utils/history.ts b/packages/media/src/react/placeholder/utils/history.ts index 4e6596472d..e112e00751 100644 --- a/packages/media/src/react/placeholder/utils/history.ts +++ b/packages/media/src/react/placeholder/utils/history.ts @@ -6,9 +6,10 @@ import { PlaceholderPlugin } from '../PlaceholderPlugin'; const historyMarks = new WeakMap(); export const withHistoryMark = (editor: PlateEditor, fn: () => void) => { + const prev = isHistoryMarking(editor); historyMarks.set(editor, true); fn(); - historyMarks.set(editor, false); + historyMarks.set(editor, prev); }; export const isHistoryMarking = (editor: PlateEditor): boolean => { From 6c6b2009424e57df59e03dc17e70fb355aeffc6d Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Tue, 19 Nov 2024 09:34:09 +0800 Subject: [PATCH 2/9] changeset --- .changeset/witty-eels-agree.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/witty-eels-agree.md diff --git a/.changeset/witty-eels-agree.md b/.changeset/witty-eels-agree.md new file mode 100644 index 0000000000..d330a568f7 --- /dev/null +++ b/.changeset/witty-eels-agree.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-media': patch +--- + +Fix editor crash when inserting media into an empty paragraph. From 043859e4d25da7622a04a8ee7f39ac71aab699d6 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Tue, 19 Nov 2024 10:36:21 +0800 Subject: [PATCH 3/9] ci --- apps/www/content/docs/media-placeholder.mdx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/www/content/docs/media-placeholder.mdx b/apps/www/content/docs/media-placeholder.mdx index d4044a7374..9ae94dc07d 100644 --- a/apps/www/content/docs/media-placeholder.mdx +++ b/apps/www/content/docs/media-placeholder.mdx @@ -116,10 +116,6 @@ To implement your own backend for file uploads: } ``` -## Examples - - - ### Plate UI Refer to the preview above. From 5de3379aa56ec97940332748df8431a264dd041b Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Tue, 19 Nov 2024 15:06:01 +0800 Subject: [PATCH 4/9] fix --- apps/www/content/docs/media-placeholder.mdx | 2 - apps/www/package.json | 2 + .../r/styles/default/api-uploadthing.json | 19 +++ .../default/media-placeholder-element.json | 2 +- .../public/r/styles/default/uploadthing.json | 16 +++ apps/www/src/__registry__/index.tsx | 24 ++++ .../components/api/uploadthing/route.ts | 12 +- .../src/registry/default/lib/uploadthing.ts | 65 ++++++--- .../plate-ui/media-placeholder-element.tsx | 5 +- apps/www/src/registry/registry-components.ts | 13 ++ apps/www/src/registry/registry-lib.ts | 11 ++ .../plate-playground-template/next.config.mjs | 1 + .../src/app/api/uploadthing/route.ts | 27 +++- .../plate-ui/media-placeholder-element.tsx | 2 +- .../src/lib/uploadthing/uploadthing.ts | 33 ++++- yarn.lock | 128 +++++++++++++++++- 16 files changed, 333 insertions(+), 29 deletions(-) create mode 100644 apps/www/public/r/styles/default/api-uploadthing.json create mode 100644 apps/www/public/r/styles/default/uploadthing.json rename templates/plate-playground-template/src/app/api/uploadthing/core.ts => apps/www/src/registry/default/components/api/uploadthing/route.ts (75%) diff --git a/apps/www/content/docs/media-placeholder.mdx b/apps/www/content/docs/media-placeholder.mdx index 9ae94dc07d..c46767ce5c 100644 --- a/apps/www/content/docs/media-placeholder.mdx +++ b/apps/www/content/docs/media-placeholder.mdx @@ -26,8 +26,6 @@ npm install @udecode/plate-media ## Usage -How to configuration the backend see [Upload](/docs/upload). - ```tsx import { AudioPlugin, diff --git a/apps/www/package.json b/apps/www/package.json index 837f4d8ab2..649de99b9e 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -131,6 +131,7 @@ "@udecode/slate-react": "workspace:^", "@udecode/slate-utils": "workspace:^", "@udecode/utils": "workspace:^", + "@uploadthing/react": "7.1.0", "@vercel/og": "^0.6.2", "ai": "^3.4.10", "class-variance-authority": "^0.7.0", @@ -172,6 +173,7 @@ "ts-morph": "^22.0.0", "unist-builder": "4.0.0", "unist-util-visit": "^5.0.0", + "uploadthing": "7.2.0", "use-file-picker": "2.1.2", "vaul": "0.9.0" }, diff --git a/apps/www/public/r/styles/default/api-uploadthing.json b/apps/www/public/r/styles/default/api-uploadthing.json new file mode 100644 index 0000000000..32a6d883c2 --- /dev/null +++ b/apps/www/public/r/styles/default/api-uploadthing.json @@ -0,0 +1,19 @@ +{ + "dependencies": [ + "uploadthing@7.2.0" + ], + "files": [ + { + "content": "import type { FileRouter } from 'uploadthing/next';\n\nimport { createRouteHandler, createUploadthing } from 'uploadthing/next';\n\nconst f = createUploadthing();\n\n// FileRouter for your app, can contain multiple FileRoutes\nconst ourFileRouter = {\n // Define as many FileRoutes as you like, each with a unique routeSlug\n imageUploader: f(['image', 'text', 'blob', 'pdf', 'video', 'audio'])\n // Set permissions and file types for this FileRoute\n .middleware(async ({ req }) => {\n // This code runs on your server before upload\n\n // Whatever is returned here is accessible in onUploadComplete as `metadata`\n return {};\n })\n .onUploadComplete(({ file, metadata }) => {\n // This code RUNS ON YOUR SERVER after upload\n\n // !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback\n return { file };\n }),\n} satisfies FileRouter;\n\nexport type OurFileRouter = typeof ourFileRouter;\n\n// Export routes for Next App Router\nexport const { GET, POST } = createRouteHandler({\n router: ourFileRouter,\n\n // Apply an (optional) custom config:\n // config: { ... },\n});\n", + "path": "components/api/uploadthing/route.ts", + "target": "app/api/uploadthing/route.ts", + "type": "registry:page" + } + ], + "name": "api-uploadthing", + "registryDependencies": [ + "media-placeholder-element", + "uploadthing" + ], + "type": "registry:component" +} \ No newline at end of file diff --git a/apps/www/public/r/styles/default/media-placeholder-element.json b/apps/www/public/r/styles/default/media-placeholder-element.json index 89118cd1d4..6e8ed0b501 100644 --- a/apps/www/public/r/styles/default/media-placeholder-element.json +++ b/apps/www/public/r/styles/default/media-placeholder-element.json @@ -23,7 +23,7 @@ }, "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: ,\n },\n [FilePlugin.key]: {\n accept: ['*'],\n content: 'Add a file',\n icon: ,\n },\n [ImagePlugin.key]: {\n accept: ['image/*'],\n content: 'Add an image',\n icon: ,\n },\n [VideoPlugin.key]: {\n accept: ['video/*'],\n content: 'Add a video',\n icon: ,\n },\n};\n\nexport const MediaPlaceholderElement = withHOC(\n PlaceholderProvider,\n withRef(\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(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 \n {(!loading || !isImage) && (\n !loading && openFilePicker()}\n contentEditable={false}\n >\n
\n {currentContent.icon}\n
\n
\n
\n {loading ? uploadingFile?.name : currentContent.content}\n
\n\n {loading && !isImage && (\n
\n
{formatBytes(uploadingFile?.size ?? 0)}
\n
\n
\n \n {progress ?? 0}%\n
\n
\n )}\n
\n \n )}\n\n {isImage && loading && (\n \n )}\n\n {children}\n \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;\n progress?: number;\n}) {\n const [objectUrl, setObjectUrl] = useState(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
\n \n {progress < 100 && (\n
\n \n \n {Math.round(progress)}%\n \n
\n )}\n
\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", + "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';\n\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: ,\n },\n [FilePlugin.key]: {\n accept: ['*'],\n content: 'Add a file',\n icon: ,\n },\n [ImagePlugin.key]: {\n accept: ['image/*'],\n content: 'Add an image',\n icon: ,\n },\n [VideoPlugin.key]: {\n accept: ['video/*'],\n content: 'Add a video',\n icon: ,\n },\n};\n\nexport const MediaPlaceholderElement = withHOC(\n PlaceholderProvider,\n withRef(\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(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 \n {(!loading || !isImage) && (\n !loading && openFilePicker()}\n contentEditable={false}\n >\n
\n {currentContent.icon}\n
\n
\n
\n {loading ? uploadingFile?.name : currentContent.content}\n
\n\n {loading && !isImage && (\n
\n
{formatBytes(uploadingFile?.size ?? 0)}
\n
\n
\n \n {progress ?? 0}%\n
\n
\n )}\n
\n \n )}\n\n {isImage && loading && (\n \n )}\n\n {children}\n \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;\n progress?: number;\n}) {\n const [objectUrl, setObjectUrl] = useState(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
\n \n {progress < 100 && (\n
\n \n \n {Math.round(progress)}%\n \n
\n )}\n
\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" diff --git a/apps/www/public/r/styles/default/uploadthing.json b/apps/www/public/r/styles/default/uploadthing.json new file mode 100644 index 0000000000..46d0c51386 --- /dev/null +++ b/apps/www/public/r/styles/default/uploadthing.json @@ -0,0 +1,16 @@ +{ + "dependencies": [ + "uploadthing@7.2.0", + "sonner" + ], + "files": [ + { + "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();\n const [uploadingFile, setUploadingFile] = React.useState();\n const [progress, setProgress] = React.useState(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, 50));\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.ts", + "target": "lib/uploadthing.ts", + "type": "registry:lib" + } + ], + "name": "uploadthing", + "type": "registry:lib" +} \ No newline at end of file diff --git a/apps/www/src/__registry__/index.tsx b/apps/www/src/__registry__/index.tsx index e3b1cfdd6b..578613da20 100644 --- a/apps/www/src/__registry__/index.tsx +++ b/apps/www/src/__registry__/index.tsx @@ -1505,6 +1505,18 @@ export const Index: Record = { subcategory: "", chunks: [] }, + "api-uploadthing": { + name: "api-uploadthing", + description: "", + type: "registry:component", + registryDependencies: ["media-placeholder-element","uploadthing"], + files: ["registry/default/components/api/uploadthing/route.ts"], + component: React.lazy(() => import("@/registry/default/components/api/uploadthing/route.ts")), + source: "", + category: "", + subcategory: "", + chunks: [] + }, "transforms": { name: "transforms", description: "", @@ -2369,6 +2381,18 @@ export const Index: Record = { subcategory: "", chunks: [] }, + "uploadthing": { + name: "uploadthing", + description: "", + type: "registry:lib", + registryDependencies: undefined, + files: ["registry/default/lib/uploadthing.ts"], + component: React.lazy(() => import("@/registry/default/lib/uploadthing.ts")), + source: "", + category: "", + subcategory: "", + chunks: [] + }, "use-debounce": { name: "use-debounce", description: "", diff --git a/templates/plate-playground-template/src/app/api/uploadthing/core.ts b/apps/www/src/registry/default/components/api/uploadthing/route.ts similarity index 75% rename from templates/plate-playground-template/src/app/api/uploadthing/core.ts rename to apps/www/src/registry/default/components/api/uploadthing/route.ts index 3233c7cb10..1a20e0732b 100644 --- a/templates/plate-playground-template/src/app/api/uploadthing/core.ts +++ b/apps/www/src/registry/default/components/api/uploadthing/route.ts @@ -1,11 +1,11 @@ import type { FileRouter } from 'uploadthing/next'; -import { createUploadthing } from 'uploadthing/next'; +import { createRouteHandler, createUploadthing } from 'uploadthing/next'; const f = createUploadthing(); // FileRouter for your app, can contain multiple FileRoutes -export const ourFileRouter = { +const ourFileRouter = { // Define as many FileRoutes as you like, each with a unique routeSlug imageUploader: f(['image', 'text', 'blob', 'pdf', 'video', 'audio']) // Set permissions and file types for this FileRoute @@ -24,3 +24,11 @@ export const ourFileRouter = { } satisfies FileRouter; export type OurFileRouter = typeof ourFileRouter; + +// Export routes for Next App Router +export const { GET, POST } = createRouteHandler({ + router: ourFileRouter, + + // Apply an (optional) custom config: + // config: { ... }, +}); diff --git a/apps/www/src/registry/default/lib/uploadthing.ts b/apps/www/src/registry/default/lib/uploadthing.ts index 261d01bd92..2240fd4b49 100644 --- a/apps/www/src/registry/default/lib/uploadthing.ts +++ b/apps/www/src/registry/default/lib/uploadthing.ts @@ -1,19 +1,31 @@ import * as React from 'react'; +import type { OurFileRouter } from '@/registry/default/components/api/uploadthing/route'; +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 { - key: string; - appUrl: string; - name: string; - size: number; - type: string; - url: string; +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() { +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); @@ -24,6 +36,31 @@ export function useUploadFile() { 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); + // Mock upload for unauthenticated users // toast.info('User not logged in. Mocking upload process.'); const mockUploadedFile = { @@ -51,15 +88,6 @@ export function useUploadFile() { setUploadedFile(mockUploadedFile); return mockUploadedFile; - } catch (error) { - const errorMessage = getErrorMessage(error); - - const message = - errorMessage.length > 0 - ? errorMessage - : 'Something went wrong, please try again later.'; - - toast.error(message); } finally { setProgress(0); setIsUploading(false); @@ -76,6 +104,9 @@ export function useUploadFile() { }; } +export const { uploadFiles, useUploadThing } = + generateReactHelpers(); + export function getErrorMessage(err: unknown) { const unknownError = 'Something went wrong, please try again later.'; diff --git a/apps/www/src/registry/default/plate-ui/media-placeholder-element.tsx b/apps/www/src/registry/default/plate-ui/media-placeholder-element.tsx index 5a3c80c232..812c9790f3 100644 --- a/apps/www/src/registry/default/plate-ui/media-placeholder-element.tsx +++ b/apps/www/src/registry/default/plate-ui/media-placeholder-element.tsx @@ -29,7 +29,8 @@ import { import { AudioLines, FileUp, Film, ImageIcon } from 'lucide-react'; import { useFilePicker } from 'use-file-picker'; -import { useUploadFile } from '../lib/uploadthing'; +import { useUploadFile } from '@/registry/default/lib/uploadthing'; + import { PlateElement } from './plate-element'; import { Spinner } from './spinner'; @@ -72,7 +73,7 @@ export const MediaPlaceholderElement = withHOC( const { api } = useEditorPlugin(PlaceholderPlugin); const { isUploading, progress, uploadFile, uploadedFile, uploadingFile } = - useUploadFile(); + useUploadFile('imageUploader'); const loading = isUploading && uploadingFile; diff --git a/apps/www/src/registry/registry-components.ts b/apps/www/src/registry/registry-components.ts index a79f70beaf..4a8495c47a 100644 --- a/apps/www/src/registry/registry-components.ts +++ b/apps/www/src/registry/registry-components.ts @@ -337,6 +337,19 @@ export const components: Registry = [ registryDependencies: ['use-chat'], type: 'registry:component', }, + { + dependencies: ['uploadthing@7.2.0'], + files: [ + { + path: 'components/api/uploadthing/route.ts', + target: 'app/api/uploadthing/route.ts', + type: 'registry:page', + }, + ], + name: 'api-uploadthing', + registryDependencies: ['media-placeholder-element', 'uploadthing'], + type: 'registry:component', + }, { dependencies: [ '@udecode/plate-callout', diff --git a/apps/www/src/registry/registry-lib.ts b/apps/www/src/registry/registry-lib.ts index 3eac18913d..216e62c2c0 100644 --- a/apps/www/src/registry/registry-lib.ts +++ b/apps/www/src/registry/registry-lib.ts @@ -12,4 +12,15 @@ export const lib: Registry = [ name: 'utils', type: 'registry:lib', }, + { + dependencies: ['uploadthing@7.2.0', 'sonner'], + files: [ + { + path: 'lib/uploadthing.ts', + type: 'registry:lib', + }, + ], + name: 'uploadthing', + type: 'registry:lib', + }, ]; diff --git a/templates/plate-playground-template/next.config.mjs b/templates/plate-playground-template/next.config.mjs index 2527337854..acdde17bae 100644 --- a/templates/plate-playground-template/next.config.mjs +++ b/templates/plate-playground-template/next.config.mjs @@ -4,6 +4,7 @@ const nextConfig = { return [ { destination: '/editor', + permanent: false, source: '/', }, ]; diff --git a/templates/plate-playground-template/src/app/api/uploadthing/route.ts b/templates/plate-playground-template/src/app/api/uploadthing/route.ts index 379d038d96..1a20e0732b 100644 --- a/templates/plate-playground-template/src/app/api/uploadthing/route.ts +++ b/templates/plate-playground-template/src/app/api/uploadthing/route.ts @@ -1,6 +1,29 @@ -import { createRouteHandler } from 'uploadthing/next'; +import type { FileRouter } from 'uploadthing/next'; -import { ourFileRouter } from './core'; +import { createRouteHandler, createUploadthing } from 'uploadthing/next'; + +const f = createUploadthing(); + +// FileRouter for your app, can contain multiple FileRoutes +const ourFileRouter = { + // Define as many FileRoutes as you like, each with a unique routeSlug + imageUploader: f(['image', 'text', 'blob', 'pdf', 'video', 'audio']) + // Set permissions and file types for this FileRoute + .middleware(async ({ req }) => { + // This code runs on your server before upload + + // Whatever is returned here is accessible in onUploadComplete as `metadata` + return {}; + }) + .onUploadComplete(({ file, metadata }) => { + // This code RUNS ON YOUR SERVER after upload + + // !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback + return { file }; + }), +} satisfies FileRouter; + +export type OurFileRouter = typeof ourFileRouter; // Export routes for Next App Router export const { GET, POST } = createRouteHandler({ 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 76713b8be0..2863b3c9df 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 @@ -29,7 +29,7 @@ import { import { AudioLines, FileUp, Film, ImageIcon } from 'lucide-react'; import { useFilePicker } from 'use-file-picker'; -import { useUploadFile } from '@/lib/uploadthing'; +import { useUploadFile } from '@/lib/uploadthing/uploadthing'; import { PlateElement } from './plate-element'; import { Spinner } from './spinner'; diff --git a/templates/plate-playground-template/src/lib/uploadthing/uploadthing.ts b/templates/plate-playground-template/src/lib/uploadthing/uploadthing.ts index c5dbe904ce..94e993e5f0 100644 --- a/templates/plate-playground-template/src/lib/uploadthing/uploadthing.ts +++ b/templates/plate-playground-template/src/lib/uploadthing/uploadthing.ts @@ -1,6 +1,5 @@ import * as React from 'react'; -import type { OurFileRouter } from '@/app/api/uploadthing/core'; import type { ClientUploadedFileData, UploadFilesOptions, @@ -11,6 +10,8 @@ import { isRedirectError } from 'next/dist/client/components/redirect'; import { toast } from 'sonner'; import { z } from 'zod'; +import { OurFileRouter } from '@/app/api/uploadthing/route'; + export interface UploadedFile extends ClientUploadedFileData {} interface UseUploadFileProps @@ -58,7 +59,36 @@ export function useUploadFile( : 'Something went wrong, please try again later.'; toast.error(message); + onUploadError?.(error); + + // Mock upload for unauthenticated users + // toast.info('User not logged in. Mocking upload process.'); + const mockUploadedFile = { + key: 'mock-key-0', + appUrl: `https://mock-app-url.com/${file.name}`, + name: file.name, + size: file.size, + type: file.type, + url: URL.createObjectURL(file), + } as UploadedFile; + + // Simulate upload progress + let progress = 0; + + const simulateProgress = async () => { + while (progress < 100) { + await new Promise((resolve) => setTimeout(resolve, 50)); + progress += 2; + setProgress(Math.min(progress, 100)); + } + }; + + await simulateProgress(); + + setUploadedFile(mockUploadedFile); + + return mockUploadedFile; } finally { setProgress(0); setIsUploading(false); @@ -74,6 +104,7 @@ export function useUploadFile( uploadingFile, }; } + export const { uploadFiles, useUploadThing } = generateReactHelpers(); diff --git a/yarn.lock b/yarn.lock index ff308896e1..af4870c5ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1135,6 +1135,18 @@ __metadata: languageName: node linkType: hard +"@effect/platform@npm:0.69.8": + version: 0.69.8 + resolution: "@effect/platform@npm:0.69.8" + dependencies: + find-my-way-ts: "npm:^0.1.5" + multipasta: "npm:^0.2.5" + peerDependencies: + effect: ^3.10.3 + checksum: 10c0/23028c02a15d8504369d301ed49f42157ebcee2bc4caddf473f35231a725dfa75eb5363add2d686d5dec58bd500559b787a61827d423fb448a6b2dddf65d605c + languageName: node + linkType: hard + "@emnapi/runtime@npm:^1.2.0": version: 1.3.1 resolution: "@emnapi/runtime@npm:1.3.1" @@ -7402,6 +7414,41 @@ __metadata: languageName: node linkType: hard +"@uploadthing/mime-types@npm:0.3.1": + version: 0.3.1 + resolution: "@uploadthing/mime-types@npm:0.3.1" + checksum: 10c0/40acf3ce6427c979e8e8a9fc60169fee35c6b6b504d8ed462d62579f9f57a940c3b0afd9e387cec03247b86772fcfc93ba7539fa07cb43a9698c18dbdff5802f + languageName: node + linkType: hard + +"@uploadthing/react@npm:7.1.0": + version: 7.1.0 + resolution: "@uploadthing/react@npm:7.1.0" + dependencies: + "@uploadthing/shared": "npm:7.1.0" + file-selector: "npm:0.6.0" + peerDependencies: + next: "*" + react: ^17.0.2 || ^18.0.0 + uploadthing: 7.2.0 + peerDependenciesMeta: + next: + optional: true + checksum: 10c0/4ef6353e38a9dcbaf3078943ddb111b5a9cc682bc4436ba3c124a8b92afcfad39c195a77592bffab567abb296598e804254b47b6507e28fd73872f4ac853c7de + languageName: node + linkType: hard + +"@uploadthing/shared@npm:7.1.0": + version: 7.1.0 + resolution: "@uploadthing/shared@npm:7.1.0" + dependencies: + "@uploadthing/mime-types": "npm:0.3.1" + effect: "npm:3.10.3" + sqids: "npm:^0.3.0" + checksum: 10c0/655d316470c288a237f6e4c749f45d81ebdaed1b3d2dc6813180fc0e500029874130b2adf9b29681cc395f435c4b4ec4061bb38c6d3abbbbb88af5282c12925c + languageName: node + linkType: hard + "@vercel/og@npm:^0.6.2": version: 0.6.3 resolution: "@vercel/og@npm:0.6.3" @@ -9646,6 +9693,15 @@ __metadata: languageName: node linkType: hard +"effect@npm:3.10.3": + version: 3.10.3 + resolution: "effect@npm:3.10.3" + dependencies: + fast-check: "npm:^3.21.0" + checksum: 10c0/aa09e50b67c9bf90e4ab11e34d8c9163de46d1ec2baa84ff8ce180f06e3b45798578cb5dca3051bb42596404f8978e24bc912790a984af1921618aef22570ee7 + languageName: node + linkType: hard + "ejs@npm:^3.1.10": version: 3.1.10 resolution: "ejs@npm:3.1.10" @@ -11118,6 +11174,15 @@ __metadata: languageName: node linkType: hard +"fast-check@npm:^3.21.0": + version: 3.23.1 + resolution: "fast-check@npm:3.23.1" + dependencies: + pure-rand: "npm:^6.1.0" + checksum: 10c0/d61ee4a7a2e1abc5126bf2f1894413f532f686b3d1fc15c67fefb60dcca66024934b69a6454d3eba92e6568ac1abbb9882080e212d255865c3b3bbe52c5bf702 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -11283,6 +11348,15 @@ __metadata: languageName: node linkType: hard +"file-selector@npm:0.6.0": + version: 0.6.0 + resolution: "file-selector@npm:0.6.0" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/477ca1b56274db9fee1a8a623c4bfef580389726a5fef843af8c1f2f17f70ec2d1e41b29115777c92e120a15f1cca734c6ef36bb48bfa2ee027c68da16cd0d28 + languageName: node + linkType: hard + "filelist@npm:^1.0.4": version: 1.0.4 resolution: "filelist@npm:1.0.4" @@ -11310,6 +11384,13 @@ __metadata: languageName: node linkType: hard +"find-my-way-ts@npm:^0.1.5": + version: 0.1.5 + resolution: "find-my-way-ts@npm:0.1.5" + checksum: 10c0/703797b8466e1fbf437f0a6f7e100e02ca5922c0d7afb0548b5e77fa9c7b4e4a5bed5d2589e27cc037a516b144e3ba334acce5369387a79975ffaf61b2900398 + languageName: node + linkType: hard + "find-process@npm:^1.4.7": version: 1.4.7 resolution: "find-process@npm:1.4.7" @@ -15913,6 +15994,13 @@ __metadata: languageName: node linkType: hard +"multipasta@npm:^0.2.5": + version: 0.2.5 + resolution: "multipasta@npm:0.2.5" + checksum: 10c0/f0790b1bdfc6b84ae6139982ead353957b668fe54fec8a4d93c19e9d051b02c837c93fbdff10b26bff2dbfcf7792c6435c27786c14ad82a0fb33b939c1bf96c6 + languageName: node + linkType: hard + "mz@npm:^2.7.0": version: 2.7.0 resolution: "mz@npm:2.7.0" @@ -17663,7 +17751,7 @@ __metadata: languageName: node linkType: hard -"pure-rand@npm:^6.0.0": +"pure-rand@npm:^6.0.0, pure-rand@npm:^6.1.0": version: 6.1.0 resolution: "pure-rand@npm:6.1.0" checksum: 10c0/1abe217897bf74dcb3a0c9aba3555fe975023147b48db540aa2faf507aee91c03bf54f6aef0eb2bf59cc259a16d06b28eca37f0dc426d94f4692aeff02fb0e65 @@ -19660,6 +19748,13 @@ __metadata: languageName: node linkType: hard +"sqids@npm:^0.3.0": + version: 0.3.0 + resolution: "sqids@npm:0.3.0" + checksum: 10c0/2ce528b83f2780166b2e8025ece1e8262ee217ed51ab5656959cb6e1a885eee5b6ea86627ce6b0321d7b0eeb34a2ed455476c01c44e6e55e18cee52b3e1b9eb3 + languageName: node + linkType: hard + "ssri@npm:^10.0.0": version: 10.0.6 resolution: "ssri@npm:10.0.6" @@ -21333,6 +21428,35 @@ __metadata: languageName: node linkType: hard +"uploadthing@npm:7.2.0": + version: 7.2.0 + resolution: "uploadthing@npm:7.2.0" + dependencies: + "@effect/platform": "npm:0.69.8" + "@uploadthing/mime-types": "npm:0.3.1" + "@uploadthing/shared": "npm:7.1.0" + effect: "npm:3.10.3" + peerDependencies: + express: "*" + fastify: "*" + h3: "*" + next: "*" + tailwindcss: "*" + peerDependenciesMeta: + express: + optional: true + fastify: + optional: true + h3: + optional: true + next: + optional: true + tailwindcss: + optional: true + checksum: 10c0/49ee09f7fefd676c02597a618cff98c08b113398571ca9ab5748c69f3b56868e0c99579639aa0e59a496377b5de533b24fc38b65ea2fa95b97d82769f05b78b8 + languageName: node + linkType: hard + "uri-js@npm:^4.2.2": version: 4.4.1 resolution: "uri-js@npm:4.4.1" @@ -22096,6 +22220,7 @@ __metadata: "@udecode/slate-react": "workspace:^" "@udecode/slate-utils": "workspace:^" "@udecode/utils": "workspace:^" + "@uploadthing/react": "npm:7.1.0" "@vercel/og": "npm:^0.6.2" ai: "npm:^3.4.10" autoprefixer: "npm:^10.4.20" @@ -22158,6 +22283,7 @@ __metadata: typescript: "npm:5.6.2" unist-builder: "npm:4.0.0" unist-util-visit: "npm:^5.0.0" + uploadthing: "npm:7.2.0" use-file-picker: "npm:2.1.2" vaul: "npm:0.9.0" languageName: unknown From 82dca4252c071008e9e9dd85fb5aa8770452c304 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Tue, 19 Nov 2024 15:12:29 +0800 Subject: [PATCH 5/9] lint fix --- .../src/app/page.tsx | 50 +++++++++---------- .../editor/plugins/indent-list-plugins.ts | 5 +- .../src/components/plate-ui/ai-menu.tsx | 4 +- .../src/components/plate-ui/avatar.tsx | 2 +- .../src/components/plate-ui/button.tsx | 6 +-- .../src/components/plate-ui/checkbox.tsx | 2 +- .../plate-ui/code-block-element.tsx | 2 +- .../src/components/plate-ui/code-leaf.tsx | 2 +- .../plate-ui/color-dropdown-menu-items.tsx | 2 +- .../plate-ui/column-group-element.tsx | 2 +- .../src/components/plate-ui/command.tsx | 14 +++--- .../src/components/plate-ui/comment-item.tsx | 2 +- .../src/components/plate-ui/comment-leaf.tsx | 2 +- .../plate-ui/comment-more-dropdown.tsx | 2 +- .../plate-ui/comment-resolve-button.tsx | 2 +- .../src/components/plate-ui/context-menu.tsx | 18 +++---- .../components/plate-ui/cursor-overlay.tsx | 2 +- .../src/components/plate-ui/date-element.tsx | 2 +- .../src/components/plate-ui/dialog.tsx | 8 +-- .../src/components/plate-ui/draggable.tsx | 4 +- .../src/components/plate-ui/dropdown-menu.tsx | 16 +++--- .../src/components/plate-ui/editor.tsx | 6 +-- .../plate-ui/emoji-picker-content.tsx | 8 +-- .../plate-ui/emoji-picker-navigation.tsx | 6 +-- .../plate-ui/emoji-picker-preview.tsx | 6 +-- .../emoji-picker-search-and-clear.tsx | 6 +-- .../plate-ui/emoji-picker-search-bar.tsx | 2 +- .../src/components/plate-ui/emoji-picker.tsx | 2 +- .../src/components/plate-ui/fixed-toolbar.tsx | 2 +- .../components/plate-ui/floating-toolbar.tsx | 2 +- .../src/components/plate-ui/ghost-text.tsx | 2 +- .../components/plate-ui/heading-element.tsx | 8 +-- .../src/components/plate-ui/hr-element.tsx | 4 +- .../components/plate-ui/inline-combobox.tsx | 8 +-- .../src/components/plate-ui/input.tsx | 4 +- .../src/components/plate-ui/kbd-leaf.tsx | 2 +- .../src/components/plate-ui/link-element.tsx | 2 +- .../plate-ui/link-floating-toolbar.tsx | 4 +- .../plate-ui/media-embed-element.tsx | 6 +-- .../components/plate-ui/mention-element.tsx | 4 +- .../plate-ui/mention-input-element.tsx | 2 +- .../src/components/plate-ui/popover.tsx | 2 +- .../src/components/plate-ui/resizable.tsx | 2 +- .../src/components/plate-ui/separator.tsx | 2 +- .../plate-ui/slash-input-element.tsx | 2 +- .../plate-ui/table-cell-element.tsx | 16 +++--- .../src/components/plate-ui/toc-element.tsx | 2 +- .../components/plate-ui/toggle-element.tsx | 2 +- .../plate-ui/turn-into-dropdown-menu.tsx | 5 +- .../src/lib/utils.ts | 6 +-- 50 files changed, 134 insertions(+), 140 deletions(-) diff --git a/templates/plate-playground-template/src/app/page.tsx b/templates/plate-playground-template/src/app/page.tsx index 433c8aa7fd..7ccfa5451a 100644 --- a/templates/plate-playground-template/src/app/page.tsx +++ b/templates/plate-playground-template/src/app/page.tsx @@ -1,21 +1,21 @@ -import Image from "next/image"; +import Image from 'next/image'; export default function Home() { return ( -
-
+
+
Next.js logo -
    +
    1. - Get started by editing{" "} - + Get started by editing{' '} + app/page.tsx . @@ -23,75 +23,75 @@ export default function Home() {
    2. Save and see your changes instantly.
    -
-