Add plugin media
This commit is contained in:
@@ -5,6 +5,9 @@ import { useParams } from "next/navigation";
|
|||||||
import { Editor } from "@reacteditor/core";
|
import { Editor } from "@reacteditor/core";
|
||||||
import createTailwindCdnPlugin from "@reacteditor/plugin-tailwind-cdn";
|
import createTailwindCdnPlugin from "@reacteditor/plugin-tailwind-cdn";
|
||||||
import { createShopifyPlugin } from "@reacteditor/plugin-shopify";
|
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 { appConfig } from "@/editor.config";
|
||||||
import resolveRoute from "@/lib/resolve-route";
|
import resolveRoute from "@/lib/resolve-route";
|
||||||
import schema from "@/app.schema.json";
|
import schema from "@/app.schema.json";
|
||||||
@@ -23,6 +26,13 @@ export default function EditorPage() {
|
|||||||
publicAccessToken:
|
publicAccessToken:
|
||||||
process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN,
|
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,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|||||||
66
lib/adapters/media-adapter.ts
Normal file
66
lib/adapters/media-adapter.ts
Normal file
@@ -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<MediaItem>((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}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -28,8 +28,8 @@
|
|||||||
"@reacteditor/core": "0.0.32",
|
"@reacteditor/core": "0.0.32",
|
||||||
"@reacteditor/field-google-fonts": "^0.0.3",
|
"@reacteditor/field-google-fonts": "^0.0.3",
|
||||||
"@reacteditor/field-shopify": "^0.0.2",
|
"@reacteditor/field-shopify": "^0.0.2",
|
||||||
"@reacteditor/plugin-ai": "^0.0.7",
|
"@reacteditor/plugin-ai": "0.0.8",
|
||||||
"@reacteditor/plugin-media": "^0.0.4",
|
"@reacteditor/plugin-media": "0.0.5",
|
||||||
"@reacteditor/plugin-shopify": "^0.0.1",
|
"@reacteditor/plugin-shopify": "^0.0.1",
|
||||||
"@reacteditor/plugin-tailwind-cdn": "^0.0.3",
|
"@reacteditor/plugin-tailwind-cdn": "^0.0.3",
|
||||||
"@shopify/storefront-api-client": "^1.0.0",
|
"@shopify/storefront-api-client": "^1.0.0",
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ import type {
|
|||||||
MediaPage,
|
MediaPage,
|
||||||
} from "@reacteditor/plugin-media";
|
} from "@reacteditor/plugin-media";
|
||||||
|
|
||||||
const MEDIA_BASE = "https://www.frontend-ai.com";
|
const FRONTEND_CLOUD = "https://cloud.frontend.co";
|
||||||
const MEDIA_API_KEY = process.env.NEXT_PUBLIC_API_KEY ?? "";
|
const FRONTEND_API_KEY = process.env.NEXT_PUBLIC_FRONTEND_API_KEY ?? "";
|
||||||
|
|
||||||
export const frontendAiMediaAdapter: MediaAdapter = {
|
export const frontendAiMediaAdapter: MediaAdapter = {
|
||||||
fetchList: async ({ query, cursor, signal }) => {
|
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 (query) url.searchParams.set("query", query);
|
||||||
if (cursor) url.searchParams.set("cursor", cursor);
|
if (cursor) url.searchParams.set("cursor", cursor);
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { "X-Api-Key": MEDIA_API_KEY },
|
headers: { "X-Api-Key": FRONTEND_API_KEY },
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`List failed: ${res.status}`);
|
if (!res.ok) throw new Error(`List failed: ${res.status}`);
|
||||||
@@ -24,8 +24,8 @@ export const frontendAiMediaAdapter: MediaAdapter = {
|
|||||||
upload: (file, opts) =>
|
upload: (file, opts) =>
|
||||||
new Promise<MediaItem>((resolve, reject) => {
|
new Promise<MediaItem>((resolve, reject) => {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open("POST", `${MEDIA_BASE}/api/media`);
|
xhr.open("POST", `${FRONTEND_CLOUD}/api/media`);
|
||||||
xhr.setRequestHeader("X-Api-Key", MEDIA_API_KEY);
|
xhr.setRequestHeader("X-Api-Key", FRONTEND_API_KEY);
|
||||||
xhr.upload.onprogress = (e) => {
|
xhr.upload.onprogress = (e) => {
|
||||||
if (e.lengthComputable) opts?.onProgress?.(e.loaded / e.total);
|
if (e.lengthComputable) opts?.onProgress?.(e.loaded / e.total);
|
||||||
};
|
};
|
||||||
@@ -54,10 +54,10 @@ export const frontendAiMediaAdapter: MediaAdapter = {
|
|||||||
|
|
||||||
delete: async (id) => {
|
delete: async (id) => {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${MEDIA_BASE}/api/media/${encodeURIComponent(id)}`,
|
`${FRONTEND_CLOUD}/api/media/${encodeURIComponent(id)}`,
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
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}`);
|
if (!res.ok) throw new Error(`Delete failed: ${res.status}`);
|
||||||
|
|||||||
16
yarn.lock
16
yarn.lock
@@ -1217,10 +1217,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@reacteditor/field-shopify/-/field-shopify-0.0.2.tgz#ee1e67f5142cebc9bb9bff816752f53b74ecaf23"
|
resolved "https://registry.yarnpkg.com/@reacteditor/field-shopify/-/field-shopify-0.0.2.tgz#ee1e67f5142cebc9bb9bff816752f53b74ecaf23"
|
||||||
integrity sha512-1ZdqK55QlGl4b2l370XWJkiGAWqJP7KjeQrAw9RU7c7w7SOPm2YlAsCeRqjdcFODLOm0ab8vfuExXseVVb54CQ==
|
integrity sha512-1ZdqK55QlGl4b2l370XWJkiGAWqJP7KjeQrAw9RU7c7w7SOPm2YlAsCeRqjdcFODLOm0ab8vfuExXseVVb54CQ==
|
||||||
|
|
||||||
"@reacteditor/plugin-ai@^0.0.7":
|
"@reacteditor/plugin-ai@0.0.8":
|
||||||
version "0.0.7"
|
version "0.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/@reacteditor/plugin-ai/-/plugin-ai-0.0.7.tgz#67e42b0e87e5629a2c7790bffa5f7fde05ce14be"
|
resolved "https://registry.yarnpkg.com/@reacteditor/plugin-ai/-/plugin-ai-0.0.8.tgz#09ce4dc80bd8e94f400601975bb0f0be18d9c6b7"
|
||||||
integrity sha512-wJpo/9kMIWyu+MppHq3Ilnvvf/3MK/eaA3TXLZ6yraKiEC7TH+2AnGSiQZxlfLkej1iALRl2e6LwdjBLMHILgw==
|
integrity sha512-6k1Nk6eJs05UlsNbsQtzDek/wRu6ZDTSd10bIw+5UCNDIn3IXBO5qoTDW8eku/7RNlIW34r/92Q2gJPIl5T1SQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ai-sdk/react" "3.0.177"
|
"@ai-sdk/react" "3.0.177"
|
||||||
ai "^6.0.0"
|
ai "^6.0.0"
|
||||||
@@ -1228,10 +1228,10 @@
|
|||||||
remark-gfm "^4.0.0"
|
remark-gfm "^4.0.0"
|
||||||
use-stick-to-bottom "^1.1.1"
|
use-stick-to-bottom "^1.1.1"
|
||||||
|
|
||||||
"@reacteditor/plugin-media@^0.0.4":
|
"@reacteditor/plugin-media@0.0.5":
|
||||||
version "0.0.4"
|
version "0.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/@reacteditor/plugin-media/-/plugin-media-0.0.4.tgz#b46ad9bd4a1f791b9c3279b53633fd9cbe26bca3"
|
resolved "https://registry.yarnpkg.com/@reacteditor/plugin-media/-/plugin-media-0.0.5.tgz#7fa53c73f30ff2521a4bec3164127b00b7fadf7d"
|
||||||
integrity sha512-UzyP0sqdrkREpFYDw6K6ObpH3AiTPswy35P24EWI222g0ylHHgp475ArYMUJILjtOTrxV+jwf2E1O4i40rHkFg==
|
integrity sha512-7FusJstm2BOEGxVg4JWWw7QMzujGVvJzMac5ESWGxh4kC1hOBcOL8bUstv/j4fD/Uj7mAjEaXbLPRUzdgn45Bw==
|
||||||
|
|
||||||
"@reacteditor/plugin-shopify@^0.0.1":
|
"@reacteditor/plugin-shopify@^0.0.1":
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user