From be39ff36817b52fc3be95049b0c3a7dcc659a79d Mon Sep 17 00:00:00 2001 From: Rami Bitar Date: Thu, 4 Jun 2026 15:59:34 -0400 Subject: [PATCH] Add plugin media --- app/editor/[[...slug]]/page.tsx | 10 +++++ lib/adapters/media-adapter.ts | 66 +++++++++++++++++++++++++++++++++ package.json | 4 +- services/media-adapter.ts | 16 ++++---- yarn.lock | 16 ++++---- 5 files changed, 94 insertions(+), 18 deletions(-) create mode 100644 lib/adapters/media-adapter.ts diff --git a/app/editor/[[...slug]]/page.tsx b/app/editor/[[...slug]]/page.tsx index 35bca04..9aee55b 100644 --- a/app/editor/[[...slug]]/page.tsx +++ b/app/editor/[[...slug]]/page.tsx @@ -5,6 +5,9 @@ import { useParams } from "next/navigation"; import { Editor } from "@reacteditor/core"; import createTailwindCdnPlugin from "@reacteditor/plugin-tailwind-cdn"; import { createShopifyPlugin } from "@reacteditor/plugin-shopify"; +import { aiPlugin } from "@reacteditor/plugin-ai"; +import { mediaPlugin } from "@reacteditor/plugin-media"; +import { mediaAdapter } from "@/lib/adapters/media-adapter"; import { appConfig } from "@/editor.config"; import resolveRoute from "@/lib/resolve-route"; import schema from "@/app.schema.json"; @@ -23,6 +26,13 @@ export default function EditorPage() { publicAccessToken: process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN, }), + aiPlugin({ + api: "https://cloud.frontend.co/api/chat", + headers: { "x-api-key": process.env.NEXT_PUBLIC_FRONTEND_API_KEY }, + }), + mediaPlugin({ + adapter: mediaAdapter, + }), ], [], ); diff --git a/lib/adapters/media-adapter.ts b/lib/adapters/media-adapter.ts new file mode 100644 index 0000000..ab382ba --- /dev/null +++ b/lib/adapters/media-adapter.ts @@ -0,0 +1,66 @@ +import type { + MediaAdapter, + MediaItem, + MediaPage, +} from "@reacteditor/plugin-media"; + +const CLOUD_BASE = "https://cloud.frontend.co"; +const FRONTEND_API_KEY = process.env.NEXT_PUBLIC_FRONTEND_API_KEY ?? ""; + +export const mediaAdapter: MediaAdapter = { + fetchList: async ({ query, cursor, signal }) => { + const url = new URL("/api/media", CLOUD_BASE); + if (query) url.searchParams.set("query", query); + if (cursor) url.searchParams.set("cursor", cursor); + + const res = await fetch(url, { + method: "GET", + headers: { "X-Api-Key": FRONTEND_API_KEY }, + signal, + }); + if (!res.ok) throw new Error(`List failed: ${res.status}`); + return (await res.json()) as MediaPage; + }, + + // XHR (not fetch) so we get real upload progress. + upload: (file, opts) => + new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open("POST", `${CLOUD_BASE}/api/media`); + xhr.setRequestHeader("X-Api-Key", FRONTEND_API_KEY); + + xhr.upload.onprogress = (e) => { + if (e.lengthComputable) opts?.onProgress?.(e.loaded / e.total); + }; + xhr.onload = () => { + if (xhr.status >= 400) { + reject(new Error(xhr.responseText || `Upload failed: ${xhr.status}`)); + return; + } + try { + resolve(JSON.parse(xhr.responseText) as MediaItem); + } catch (err) { + reject(err instanceof Error ? err : new Error(String(err))); + } + }; + xhr.onerror = () => reject(new Error("Network error")); + xhr.onabort = () => { + const err = new Error("Aborted"); + err.name = "AbortError"; + reject(err); + }; + opts?.signal?.addEventListener("abort", () => xhr.abort()); + + const fd = new FormData(); + fd.append("file", file); + xhr.send(fd); + }), + + delete: async (id) => { + const res = await fetch( + `${CLOUD_BASE}/api/media/${encodeURIComponent(id)}`, + { method: "DELETE", headers: { "X-Api-Key": FRONTEND_API_KEY } }, + ); + if (!res.ok) throw new Error(`Delete failed: ${res.status}`); + }, +}; diff --git a/package.json b/package.json index 6b033db..5301f69 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,8 @@ "@reacteditor/core": "0.0.32", "@reacteditor/field-google-fonts": "^0.0.3", "@reacteditor/field-shopify": "^0.0.2", - "@reacteditor/plugin-ai": "^0.0.7", - "@reacteditor/plugin-media": "^0.0.4", + "@reacteditor/plugin-ai": "0.0.8", + "@reacteditor/plugin-media": "0.0.5", "@reacteditor/plugin-shopify": "^0.0.1", "@reacteditor/plugin-tailwind-cdn": "^0.0.3", "@shopify/storefront-api-client": "^1.0.0", diff --git a/services/media-adapter.ts b/services/media-adapter.ts index af9a8a7..721ed7b 100644 --- a/services/media-adapter.ts +++ b/services/media-adapter.ts @@ -4,17 +4,17 @@ import type { MediaPage, } from "@reacteditor/plugin-media"; -const MEDIA_BASE = "https://www.frontend-ai.com"; -const MEDIA_API_KEY = process.env.NEXT_PUBLIC_API_KEY ?? ""; +const FRONTEND_CLOUD = "https://cloud.frontend.co"; +const FRONTEND_API_KEY = process.env.NEXT_PUBLIC_FRONTEND_API_KEY ?? ""; export const frontendAiMediaAdapter: MediaAdapter = { fetchList: async ({ query, cursor, signal }) => { - const url = new URL("/api/media", MEDIA_BASE); + const url = new URL("/api/media", FRONTEND_CLOUD); if (query) url.searchParams.set("query", query); if (cursor) url.searchParams.set("cursor", cursor); const res = await fetch(url, { method: "GET", - headers: { "X-Api-Key": MEDIA_API_KEY }, + headers: { "X-Api-Key": FRONTEND_API_KEY }, signal, }); if (!res.ok) throw new Error(`List failed: ${res.status}`); @@ -24,8 +24,8 @@ export const frontendAiMediaAdapter: MediaAdapter = { upload: (file, opts) => new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); - xhr.open("POST", `${MEDIA_BASE}/api/media`); - xhr.setRequestHeader("X-Api-Key", MEDIA_API_KEY); + xhr.open("POST", `${FRONTEND_CLOUD}/api/media`); + xhr.setRequestHeader("X-Api-Key", FRONTEND_API_KEY); xhr.upload.onprogress = (e) => { if (e.lengthComputable) opts?.onProgress?.(e.loaded / e.total); }; @@ -54,10 +54,10 @@ export const frontendAiMediaAdapter: MediaAdapter = { delete: async (id) => { const res = await fetch( - `${MEDIA_BASE}/api/media/${encodeURIComponent(id)}`, + `${FRONTEND_CLOUD}/api/media/${encodeURIComponent(id)}`, { method: "DELETE", - headers: { "X-Api-Key": MEDIA_API_KEY }, + headers: { "X-Api-Key": FRONTEND_API_KEY }, }, ); if (!res.ok) throw new Error(`Delete failed: ${res.status}`); diff --git a/yarn.lock b/yarn.lock index f71a751..c5e491d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1217,10 +1217,10 @@ resolved "https://registry.yarnpkg.com/@reacteditor/field-shopify/-/field-shopify-0.0.2.tgz#ee1e67f5142cebc9bb9bff816752f53b74ecaf23" integrity sha512-1ZdqK55QlGl4b2l370XWJkiGAWqJP7KjeQrAw9RU7c7w7SOPm2YlAsCeRqjdcFODLOm0ab8vfuExXseVVb54CQ== -"@reacteditor/plugin-ai@^0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@reacteditor/plugin-ai/-/plugin-ai-0.0.7.tgz#67e42b0e87e5629a2c7790bffa5f7fde05ce14be" - integrity sha512-wJpo/9kMIWyu+MppHq3Ilnvvf/3MK/eaA3TXLZ6yraKiEC7TH+2AnGSiQZxlfLkej1iALRl2e6LwdjBLMHILgw== +"@reacteditor/plugin-ai@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@reacteditor/plugin-ai/-/plugin-ai-0.0.8.tgz#09ce4dc80bd8e94f400601975bb0f0be18d9c6b7" + integrity sha512-6k1Nk6eJs05UlsNbsQtzDek/wRu6ZDTSd10bIw+5UCNDIn3IXBO5qoTDW8eku/7RNlIW34r/92Q2gJPIl5T1SQ== dependencies: "@ai-sdk/react" "3.0.177" ai "^6.0.0" @@ -1228,10 +1228,10 @@ remark-gfm "^4.0.0" use-stick-to-bottom "^1.1.1" -"@reacteditor/plugin-media@^0.0.4": - version "0.0.4" - resolved "https://registry.yarnpkg.com/@reacteditor/plugin-media/-/plugin-media-0.0.4.tgz#b46ad9bd4a1f791b9c3279b53633fd9cbe26bca3" - integrity sha512-UzyP0sqdrkREpFYDw6K6ObpH3AiTPswy35P24EWI222g0ylHHgp475ArYMUJILjtOTrxV+jwf2E1O4i40rHkFg== +"@reacteditor/plugin-media@0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@reacteditor/plugin-media/-/plugin-media-0.0.5.tgz#7fa53c73f30ff2521a4bec3164127b00b7fadf7d" + integrity sha512-7FusJstm2BOEGxVg4JWWw7QMzujGVvJzMac5ESWGxh4kC1hOBcOL8bUstv/j4fD/Uj7mAjEaXbLPRUzdgn45Bw== "@reacteditor/plugin-shopify@^0.0.1": version "0.0.1"