From 3eea07fe8b429ee49410c111e7551e88d0ea7b33 Mon Sep 17 00:00:00 2001 From: Felix Feng Date: Sun, 17 Nov 2024 16:55:54 +0800 Subject: [PATCH] uploadthing --- .../plate-playground-template/.env.example | 2 + templates/plate-playground-template/README.md | 1 + .../plate-playground-template/package.json | 2 + .../plate-playground-template/pnpm-lock.yaml | 124 ++++++++++++++++++ .../src/app/api/uploadthing/core.ts | 26 ++++ .../src/app/api/uploadthing/route.ts | 11 ++ .../src/components/plate-ui/image-element.tsx | 16 ++- .../plate-ui/media-placeholder-element.tsx | 2 +- .../src/lib/uploadthing/index.ts | 2 + .../src/lib/uploadthing/uploadthing.ts | 6 + .../src/lib/uploadthing/use-upload-file.ts | 61 +++++---- 11 files changed, 219 insertions(+), 34 deletions(-) create mode 100644 templates/plate-playground-template/.env.example create mode 100644 templates/plate-playground-template/src/app/api/uploadthing/core.ts create mode 100644 templates/plate-playground-template/src/app/api/uploadthing/route.ts create mode 100644 templates/plate-playground-template/src/lib/uploadthing/uploadthing.ts diff --git a/templates/plate-playground-template/.env.example b/templates/plate-playground-template/.env.example new file mode 100644 index 000000000..372a447bc --- /dev/null +++ b/templates/plate-playground-template/.env.example @@ -0,0 +1,2 @@ +OPENAI_API_KEY= +UPLOADTHING_TOKEN= \ No newline at end of file diff --git a/templates/plate-playground-template/README.md b/templates/plate-playground-template/README.md index da3bec43a..508e72eb4 100644 --- a/templates/plate-playground-template/README.md +++ b/templates/plate-playground-template/README.md @@ -41,6 +41,7 @@ cp .env.example .env.local Configure `.env.local`: - `OPENAI_API_KEY` – OpenAI API key ([get one here](https://platform.openai.com/account/api-keys)) +- `UPLOADTHING_TOKEN` – UploadThing API key ([get one here](https://uploadthing.com/dashboard)) You can also using your own backend Start the development server: diff --git a/templates/plate-playground-template/package.json b/templates/plate-playground-template/package.json index 564a6538b..f8012fee3 100644 --- a/templates/plate-playground-template/package.json +++ b/templates/plate-playground-template/package.json @@ -73,6 +73,8 @@ "@udecode/plate-table": "^40.0.0", "@udecode/plate-toggle": "^40.0.0", "@udecode/plate-trailing-block": "^40.0.0", + "@uploadthing/react": "7.1.0", + "uploadthing": "7.2.0", "zod": "^3.23.8", "react-player": "^2.16.0", "sonner": "^1.5.0", diff --git a/templates/plate-playground-template/pnpm-lock.yaml b/templates/plate-playground-template/pnpm-lock.yaml index 2ec56219c..03e34bca6 100644 --- a/templates/plate-playground-template/pnpm-lock.yaml +++ b/templates/plate-playground-template/pnpm-lock.yaml @@ -188,6 +188,9 @@ importers: '@udecode/plate-trailing-block': specifier: ^40.0.0 version: 40.0.0(@udecode/plate-common@40.0.3(@types/react@18.3.12)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(scheduler@0.23.2)(slate-dom@0.111.0(slate@0.110.2))(slate-history@0.110.3(slate@0.110.2))(slate-hyperscript@0.100.0(slate@0.110.2))(slate-react@0.111.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(slate-dom@0.111.0(slate@0.110.2))(slate@0.110.2))(slate@0.110.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(slate-dom@0.111.0(slate@0.110.2))(slate-history@0.110.3(slate@0.110.2))(slate-hyperscript@0.100.0(slate@0.110.2))(slate-react@0.111.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(slate-dom@0.111.0(slate@0.110.2))(slate@0.110.2))(slate@0.110.2) + '@uploadthing/react': + specifier: 7.1.0 + version: 7.1.0(next@15.0.3(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(uploadthing@7.2.0(next@15.0.3(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(tailwindcss@3.4.14)) ai: specifier: ^3.4.33 version: 3.4.33(react@18.3.1)(sswr@2.1.0(svelte@5.2.0))(svelte@5.2.0)(vue@3.5.12(typescript@5.6.3))(zod@3.23.8) @@ -266,6 +269,9 @@ importers: tailwindcss-animate: specifier: 1.0.7 version: 1.0.7(tailwindcss@3.4.14) + uploadthing: + specifier: 7.2.0 + version: 7.2.0(next@15.0.3(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(tailwindcss@3.4.14) use-file-picker: specifier: ^2.1.2 version: 2.1.2(react@18.3.1) @@ -444,6 +450,11 @@ packages: resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} engines: {node: '>=6.9.0'} + '@effect/platform@0.69.8': + resolution: {integrity: sha512-zhBhg0c1MHMMo+grOc/6wC2/3UETLroruwrYNZ89uDtXl6EOcP5alFP+vW3NToKDA2o0hRh22KNqq4aixA7xXg==} + peerDependencies: + effect: ^3.10.3 + '@emnapi/runtime@1.3.1': resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} @@ -2078,6 +2089,22 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@uploadthing/mime-types@0.3.1': + resolution: {integrity: sha512-CaEadjn33CzPSLRaU8uL8IRv8MpW9xU5Rg/R45T5In8608dzovDDk0uQ9jzmmLYU5hHt+4v2qugcG/jirm/KEA==} + + '@uploadthing/react@7.1.0': + resolution: {integrity: sha512-xySIeTkX0/nYoBA+zC4ze7ickq6TB9LB6793J00iK5ELnfIILjUtR2Uyx17dMyxMkP9lmw4wIjLCwjW4dU1GIw==} + peerDependencies: + next: '*' + react: ^17.0.2 || ^18.0.0 + uploadthing: 7.2.0 + peerDependenciesMeta: + next: + optional: true + + '@uploadthing/shared@7.1.0': + resolution: {integrity: sha512-6cdS2hq9jUJFU/tqRKHs5XsDIwc6HdaVI4ka0vRy+IwjPnQBR0iXHwqyCtNNssCCAq4zQxrZr3iNEDPNqc0dqw==} + '@vue/compiler-core@3.5.12': resolution: {integrity: sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==} @@ -2518,6 +2545,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + effect@3.10.3: + resolution: {integrity: sha512-+Z5bUhzTeqYlfoPsfXMZG1pYadqLBKARD3xwMIoEAESsOhKFOrUsHHNCy2ZZW3/6oa4wokgT01k1zavA4BAQ4w==} + electron-to-chromium@1.5.25: resolution: {integrity: sha512-kMb204zvK3PsSlgvvwzI3wBIcAw15tRkYk+NQdsjdDtcQWTp2RABbMQ9rUBy8KNEOM+/E6ep+XC3AykiWZld4g==} @@ -2756,6 +2786,10 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-check@3.23.1: + resolution: {integrity: sha512-u/MudsoQEgBUZgR5N1v87vEgybeVYus9VnDVaIkxkkGP2jt54naghQ3PCQHJiogS8U/GavZCUPFfx3Xkp+NaHw==} + engines: {node: '>=8.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2784,10 +2818,17 @@ packages: resolution: {integrity: sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==} engines: {node: '>= 10'} + file-selector@0.6.0: + resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==} + engines: {node: '>= 12'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-my-way-ts@0.1.5: + resolution: {integrity: sha512-4GOTMrpGQVzsCH2ruUn2vmwzV/02zF4q+ybhCIrw/Rkt3L8KWcycdC6aJMctJzwN4fXD4SD5F/4B9Sksh5rE0A==} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -3392,6 +3433,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + multipasta@0.2.5: + resolution: {integrity: sha512-c8eMDb1WwZcE02WVjHoOmUVk7fnKU/RmUcosHACglrWAuPQsEJv+E8430sXj6jNc1jHw0zrS16aCjQh4BcEb4A==} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -3641,6 +3685,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -3925,6 +3972,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + sqids@0.3.0: + resolution: {integrity: sha512-lOQK1ucVg+W6n3FhRwwSeUijxe93b51Bfz5PMRMihVf1iVkl82ePQG7V5vwrhzB11v0NtsR25PSZRGiSomJaJw==} + sswr@2.1.0: resolution: {integrity: sha512-Cqc355SYlTAaUt8iDPaC/4DPPXK925PePLMxyBKuWd5kKc5mwsG3nT9+Mq2tyguL5s7b4Jg+IRMpTRsNTAfpSQ==} peerDependencies: @@ -4159,6 +4209,27 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uploadthing@7.2.0: + resolution: {integrity: sha512-x7UAumRF/o+zAkHDP8Re7Qzi3pQF44BZkpsDdubjOE5lNcLw5RQD8WzUPwXKR0hsWEZcR4uoB8LNEDIHT7lAHw==} + engines: {node: '>=18.13.0'} + peerDependencies: + express: '*' + fastify: '*' + h3: '*' + next: '*' + tailwindcss: '*' + peerDependenciesMeta: + express: + optional: true + fastify: + optional: true + h3: + optional: true + next: + optional: true + tailwindcss: + optional: true + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -4455,6 +4526,12 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@effect/platform@0.69.8(effect@3.10.3)': + dependencies: + effect: 3.10.3 + find-my-way-ts: 0.1.5 + multipasta: 0.2.5 + '@emnapi/runtime@1.3.1': dependencies: tslib: 2.8.1 @@ -6053,6 +6130,23 @@ snapshots: '@ungap/structured-clone@1.2.0': {} + '@uploadthing/mime-types@0.3.1': {} + + '@uploadthing/react@7.1.0(next@15.0.3(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(uploadthing@7.2.0(next@15.0.3(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(tailwindcss@3.4.14))': + dependencies: + '@uploadthing/shared': 7.1.0 + file-selector: 0.6.0 + react: 18.3.1 + uploadthing: 7.2.0(next@15.0.3(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(tailwindcss@3.4.14) + optionalDependencies: + next: 15.0.3(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + '@uploadthing/shared@7.1.0': + dependencies: + '@uploadthing/mime-types': 0.3.1 + effect: 3.10.3 + sqids: 0.3.0 + '@vue/compiler-core@3.5.12': dependencies: '@babel/parser': 7.26.2 @@ -6542,6 +6636,10 @@ snapshots: eastasianwidth@0.2.0: {} + effect@3.10.3: + dependencies: + fast-check: 3.23.1 + electron-to-chromium@1.5.25: {} emoji-regex@8.0.0: {} @@ -6930,6 +7028,10 @@ snapshots: extend@3.0.2: {} + fast-check@3.23.1: + dependencies: + pure-rand: 6.1.0 + fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -6964,10 +7066,16 @@ snapshots: dependencies: tslib: 2.8.1 + file-selector@0.6.0: + dependencies: + tslib: 2.8.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 + find-my-way-ts@0.1.5: {} + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -7725,6 +7833,8 @@ snapshots: ms@2.1.3: {} + multipasta@0.2.5: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -7949,6 +8059,8 @@ snapshots: punycode@2.3.1: {} + pure-rand@6.1.0: {} + queue-microtask@1.2.3: {} raf@3.4.1: @@ -8303,6 +8415,8 @@ snapshots: source-map-js@1.2.1: {} + sqids@0.3.0: {} + sswr@2.1.0(svelte@5.2.0): dependencies: svelte: 5.2.0 @@ -8604,6 +8718,16 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.0 + uploadthing@7.2.0(next@15.0.3(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(tailwindcss@3.4.14): + dependencies: + '@effect/platform': 0.69.8(effect@3.10.3) + '@uploadthing/mime-types': 0.3.1 + '@uploadthing/shared': 7.1.0 + effect: 3.10.3 + optionalDependencies: + next: 15.0.3(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwindcss: 3.4.14 + uri-js@4.4.1: dependencies: punycode: 2.3.1 diff --git a/templates/plate-playground-template/src/app/api/uploadthing/core.ts b/templates/plate-playground-template/src/app/api/uploadthing/core.ts new file mode 100644 index 000000000..3233c7cb1 --- /dev/null +++ b/templates/plate-playground-template/src/app/api/uploadthing/core.ts @@ -0,0 +1,26 @@ +import type { FileRouter } from 'uploadthing/next'; + +import { createUploadthing } from 'uploadthing/next'; + +const f = createUploadthing(); + +// FileRouter for your app, can contain multiple FileRoutes +export 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; diff --git a/templates/plate-playground-template/src/app/api/uploadthing/route.ts b/templates/plate-playground-template/src/app/api/uploadthing/route.ts new file mode 100644 index 000000000..379d038d9 --- /dev/null +++ b/templates/plate-playground-template/src/app/api/uploadthing/route.ts @@ -0,0 +1,11 @@ +import { createRouteHandler } from 'uploadthing/next'; + +import { ourFileRouter } from './core'; + +// Export routes for Next App Router +export const { GET, POST } = createRouteHandler({ + router: ourFileRouter, + + // Apply an (optional) custom config: + // config: { ... }, +}); diff --git a/templates/plate-playground-template/src/components/plate-ui/image-element.tsx b/templates/plate-playground-template/src/components/plate-ui/image-element.tsx index d5915ce91..34e3f8930 100644 --- a/templates/plate-playground-template/src/components/plate-ui/image-element.tsx +++ b/templates/plate-playground-template/src/components/plate-ui/image-element.tsx @@ -3,7 +3,8 @@ import React from 'react'; import { cn, withRef } from '@udecode/cn'; -import { withHOC } from '@udecode/plate-common/react'; +import { useEditorRef, withHOC } from '@udecode/plate-common/react'; +import { useDraggable, useDraggableState } from '@udecode/plate-dnd'; import { Image, ImagePlugin, useMediaState } from '@udecode/plate-media/react'; import { ResizableProvider, useResizableStore } from '@udecode/plate-resizable'; @@ -20,10 +21,19 @@ export const ImageElement = withHOC( ResizableProvider, withRef( ({ children, className, nodeProps, ...props }, ref) => { + const editor = useEditorRef(); + const { align = 'center', focused, readOnly, selected } = useMediaState(); const width = useResizableStore().get.width(); + const state = editor.plugins.dnd + ? useDraggableState({ element: props.element }) + : ({} as any); + + const { isDragging } = state; + const { handleRef } = useDraggable(state); + return ( (); 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 index 1c708ec34..d4d62b6ae 100644 --- a/templates/plate-playground-template/src/lib/uploadthing/use-upload-file.ts +++ b/templates/plate-playground-template/src/lib/uploadthing/use-upload-file.ts @@ -1,19 +1,31 @@ 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 {} -export interface UploadedFile { - key: string; - appUrl: string; - name: string; - size: number; - type: string; - url: string; +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,33 +36,19 @@ export function useUploadFile() { setUploadingFile(file); try { - // 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, 100)); - progress += 2; + const res = await uploadFiles(endpoint, { + ...props, + files: [file], + onUploadProgress: ({ progress }) => { setProgress(Math.min(progress, 100)); - } - }; + }, + }); - await simulateProgress(); + setUploadedFile(res[0]); - setUploadedFile(mockUploadedFile); + onUploadComplete?.(res[0]); - return mockUploadedFile; + return uploadedFile; } catch (error) { const errorMessage = getErrorMessage(error); @@ -60,6 +58,7 @@ export function useUploadFile() { : 'Something went wrong, please try again later.'; toast.error(message); + onUploadError?.(error); } finally { setProgress(0); setIsUploading(false);