diff --git a/apps/example/.env.sample b/apps/example/.env.sample
deleted file mode 100644
index 8b0fbc03..00000000
--- a/apps/example/.env.sample
+++ /dev/null
@@ -1,2 +0,0 @@
-NEXT_PUBLIC_EASYBLOCKS_ACCESS_TOKEN=
-NEXT_PUBLIC_EASYBLOCKS_PROJECT_ID=
\ No newline at end of file
diff --git a/apps/example/src/app/(system)/entries/[id]/page.tsx b/apps/example/src/app/(system)/entries/[id]/page.tsx
new file mode 100644
index 00000000..b7c59eda
--- /dev/null
+++ b/apps/example/src/app/(system)/entries/[id]/page.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import { Fragment, useEffect } from "react";
+import { useMockService } from "@/data/MockData/useMockService";
+import { DocumentWidget } from "@/app/document-widget";
+import Link from "next/link";
+import { notFound } from "next/navigation";
+
+export type EntryPageParams = {
+ id: string;
+};
+export default function EntryPage({
+ params: { id },
+}: {
+ params: EntryPageParams;
+}) {
+ const mockService = useMockService();
+
+ if (!mockService) {
+ return null;
+ }
+
+ const entry = mockService.getEntryById(id);
+
+ if (!entry) {
+ notFound();
+ }
+
+ return (
+
+
+
+ ← All Entries
+
+
{entry.name}
+
+
+
+
+
+
+
Description
+
{entry.description}
+
+
+
+
Last modified
+
{entry.updatedAt.toString()}
+
+
+
+
Created at
+
{entry.createdAt.toString()}
+
+
+
+
Page
+
+ {
+ mockService.updateEntry({
+ ...entry,
+ page: document,
+ });
+ }}
+ />
+
+
+
+
+ );
+}
+
+export const revalidate = 0;
diff --git a/apps/example/src/app/(system)/entries/page.tsx b/apps/example/src/app/(system)/entries/page.tsx
new file mode 100644
index 00000000..1637e910
--- /dev/null
+++ b/apps/example/src/app/(system)/entries/page.tsx
@@ -0,0 +1,55 @@
+"use client";
+
+import { Fragment, useEffect } from "react";
+import { useMockService } from "@/data/MockData/useMockService";
+import Link from "next/link";
+
+export default function EntriesPage() {
+ const service = useMockService();
+
+ if (!service) {
+ return null;
+ }
+
+ const entries = service.getEntries();
+
+ const cellClasses = "p-2 text-left border border-gray-200 leading-none";
+
+ return (
+
+
+
System entries
+
+
+
+
+ | Name |
+ Id |
+ Last modified |
+
+
+
+ {entries.map((entry) => (
+
+ |
+
+ {entry.name}
+
+ |
+ {entry.id} |
+
+ {entry.updatedAt.toDateString()}
+ |
+
+ ))}
+
+
+
+
+ );
+}
+
+export const revalidate = 0;
diff --git a/apps/example/src/app/(system)/layout.tsx b/apps/example/src/app/(system)/layout.tsx
new file mode 100644
index 00000000..826fa119
--- /dev/null
+++ b/apps/example/src/app/(system)/layout.tsx
@@ -0,0 +1,38 @@
+import "../globals.css";
+import type { Metadata } from "next";
+import { Inter } from "next/font/google";
+
+const inter = Inter({ subsets: ["latin"] });
+
+export const metadata: Metadata = {
+ title: "ACME Software",
+ description: "ACME Software using Easyblocks",
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/apps/example/src/app/(system)/not-found.tsx b/apps/example/src/app/(system)/not-found.tsx
new file mode 100644
index 00000000..36b07446
--- /dev/null
+++ b/apps/example/src/app/(system)/not-found.tsx
@@ -0,0 +1,9 @@
+import Link from "next/link";
+
+export default function NotFound() {
+ return (
+
+
Could not find requested resource
+
+ );
+}
diff --git a/apps/example/src/app/(system)/page.tsx b/apps/example/src/app/(system)/page.tsx
new file mode 100644
index 00000000..bd3d1f75
--- /dev/null
+++ b/apps/example/src/app/(system)/page.tsx
@@ -0,0 +1,16 @@
+import Link from "next/link";
+
+export default function MainPage() {
+ return (
+
+
Hello
+
+
+ System entries
+
+
+ );
+}
diff --git a/apps/example/src/app/document-preview.tsx b/apps/example/src/app/document-preview.tsx
deleted file mode 100644
index 08cd836b..00000000
--- a/apps/example/src/app/document-preview.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-"use client";
-
-import { ContextParams } from "@easyblocks/core";
-
-function DocumentPreview({
- configId,
- accessToken,
- version,
-}: {
- configId: string;
- accessToken: string;
- version?: number;
-}) {
- return (
-
- );
-}
-
-export { DocumentPreview };
-
-function getComponentConfigPreviewImageURL({
- configId,
- contextParams,
- accessToken,
- version,
-}: {
- configId: string;
- version?: number;
- accessToken: string;
- contextParams: ContextParams;
-}): string | undefined {
- // we generate screenshot only if template config is remote
- const url = getConfigPreviewUrl({
- configId,
- version,
- accessToken,
- contextParams: {
- ...contextParams,
- locale: "en-US",
- },
- });
-
- const searchParams = new URLSearchParams();
- searchParams.set("access_key", "yeKG1SmUryGzuw");
- searchParams.set("url", url);
- searchParams.set("cache", "true");
- searchParams.set("format", "jpg");
- searchParams.set("block_cookie_banners", "true");
- searchParams.set("block_trackers", "true");
- searchParams.set("device_scale_factor", "1");
- searchParams.set("selector", "#__shopstory-container");
-
- const screenshotPreviewUrl = `https://api.screenshotone.com/take?${searchParams.toString()}`;
-
- if (isLocalhost()) {
- console.log(`(localhost) Preview URL: ${url}`);
- return;
- }
-
- return screenshotPreviewUrl;
-}
-
-function getConfigPreviewUrl({
- configId,
- accessToken,
- version,
- contextParams,
-}: {
- configId: string;
- version?: number;
- accessToken: string;
- contextParams: ContextParams;
-}): string {
- // 1. Get current URL
- const currentUrl = window.location.href;
-
- // 2. Remove query params
- const urlWithoutQueryParams = currentUrl.split("?")[0];
-
- // 3. Add new query param
- const searchParams = new URLSearchParams();
- searchParams.set("configId", configId);
- if (version) {
- searchParams.set("version", version.toString());
- }
- searchParams.set("accessToken", accessToken);
- searchParams.set(
- "contextParams",
- encodeURIComponent(JSON.stringify(contextParams))
- );
- searchParams.set("mode", "app");
-
- // 4. Generate new URL string
- const newUrl = `${urlWithoutQueryParams}shopstory-canvas?${searchParams.toString()}`;
- return newUrl;
-}
-
-function isLocalhost() {
- return (
- location.hostname === "localhost" ||
- location.hostname === "127.0.0.1" ||
- location.hostname === ""
- );
-}
diff --git a/apps/example/src/app/document-widget.tsx b/apps/example/src/app/document-widget.tsx
new file mode 100644
index 00000000..189d3bc4
--- /dev/null
+++ b/apps/example/src/app/document-widget.tsx
@@ -0,0 +1,103 @@
+"use client";
+
+import {
+ Dialog,
+ DialogContent,
+ DialogOverlay,
+ DialogPortal,
+ DialogTrigger,
+} from "@radix-ui/react-dialog";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import { generatePreviewUrl } from "@/utils/generatePreviewUrl";
+
+const DocumentWidget: React.FC<{
+ document: any;
+ onSave: (document: any) => void;
+}> = ({ document, onSave }) => {
+ const router = useRouter();
+
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
+
+ const [editorIframeNode, setEditorIframeNode] =
+ useState(null);
+
+ useEffect(() => {
+ function handleMessage(event: MessageEvent) {
+ if (event.data.type === "@easyblocks/closed") {
+ window.removeEventListener("message", handleMessage);
+ setIsDialogOpen(false);
+ router.refresh();
+ }
+
+ if (event.data.type === "@easyblocks/content-saved") {
+ onSave(event.data.document);
+ }
+ }
+
+ editorIframeNode?.contentWindow?.addEventListener("message", handleMessage);
+
+ return () => {
+ editorIframeNode?.contentWindow?.removeEventListener(
+ "message",
+ handleMessage
+ );
+ };
+ }, [editorIframeNode, router]);
+
+ let canvasUrl = `${
+ window.location.origin
+ }/shopstory-canvas?rootContainer=content&mode=app&source=sales-app&contextParams=${JSON.stringify(
+ { locale: "en-US" }
+ )}`;
+
+ if (document) {
+ canvasUrl += `&documentId=${document.id}`;
+ }
+
+ return (
+
+ {document && (
+
+

+
+ )}
+
+
+
+ );
+};
+
+export { DocumentWidget };
diff --git a/apps/example/src/app/edit-document-button.tsx b/apps/example/src/app/edit-document-button.tsx
deleted file mode 100644
index 2772c777..00000000
--- a/apps/example/src/app/edit-document-button.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-"use client";
-
-import {
- Dialog,
- DialogContent,
- DialogOverlay,
- DialogPortal,
- DialogTrigger,
-} from "@radix-ui/react-dialog";
-import { useRouter } from "next/navigation";
-import { useEffect, useRef, useState } from "react";
-import { accessToken } from "./lib/apiClient";
-
-function EditDocumentButton({ documentId }: { documentId: string }) {
- const router = useRouter();
- const [isDialogOpen, setIsDialogOpen] = useState(false);
- const [editorIframeNode, setEditorIframeNode] =
- useState(null);
-
- useEffect(() => {
- function handleMessage(event: MessageEvent) {
- if (event.data.type === "@shopstory-editor/closed") {
- window.removeEventListener("message", handleMessage);
- setIsDialogOpen(false);
- router.refresh();
- }
- }
-
- editorIframeNode?.contentWindow?.addEventListener("message", handleMessage);
-
- return () => {
- editorIframeNode?.contentWindow?.removeEventListener(
- "message",
- handleMessage
- );
- };
- }, [editorIframeNode, router]);
-
- return (
-
- );
-}
-
-export { EditDocumentButton };
diff --git a/apps/example/src/app/layout.tsx b/apps/example/src/app/layout.tsx
deleted file mode 100644
index 57322998..00000000
--- a/apps/example/src/app/layout.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import "./globals.css";
-import type { Metadata } from "next";
-import { Inter } from "next/font/google";
-
-const inter = Inter({ subsets: ["latin"] });
-
-export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
-};
-
-export default function RootLayout({
- children,
-}: {
- children: React.ReactNode;
-}) {
- return (
-
- {children}
-
- );
-}
diff --git a/apps/example/src/app/new-document-button.tsx b/apps/example/src/app/new-document-button.tsx
deleted file mode 100644
index 21d54f03..00000000
--- a/apps/example/src/app/new-document-button.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-"use client";
-
-import {
- Dialog,
- DialogContent,
- DialogOverlay,
- DialogPortal,
- DialogTrigger,
-} from "@radix-ui/react-dialog";
-import { useRouter } from "next/navigation";
-import { useEffect, useState } from "react";
-import { accessToken } from "./lib/apiClient";
-
-function NewDocument() {
- const router = useRouter();
- const [isDialogOpen, setIsDialogOpen] = useState(false);
-
- const [editorIframeNode, setEditorIframeNode] =
- useState(null);
-
- useEffect(() => {
- function handleMessage(event: MessageEvent) {
- if (event.data.type === "@shopstory-editor/closed") {
- window.removeEventListener("message", handleMessage);
- setIsDialogOpen(false);
- router.refresh();
- }
- }
-
- editorIframeNode?.contentWindow?.addEventListener("message", handleMessage);
-
- return () => {
- editorIframeNode?.contentWindow?.removeEventListener(
- "message",
- handleMessage
- );
- };
- }, [editorIframeNode, router]);
-
- const canvasUrl = `/shopstory-canvas?rootContainer=content&mode=app&source=sales-app&contextParams=${JSON.stringify(
- { locale: "en-US" }
- )}`;
-
- return (
-
- );
-}
-
-export { NewDocument };
diff --git a/apps/example/src/app/page.tsx b/apps/example/src/app/page.tsx
deleted file mode 100644
index 500144f7..00000000
--- a/apps/example/src/app/page.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import formatRelative from "date-fns/formatRelative";
-import { Fragment } from "react";
-import { DocumentPreview } from "./document-preview";
-import { EditDocumentButton } from "./edit-document-button";
-import { accessToken, createApiClient } from "./lib/apiClient";
-import { NewDocument } from "./new-document-button";
-
-export default async function Home() {
- const apiClient = createApiClient();
-
- if (!process.env.NEXT_PUBLIC_EASYBLOCKS_PROJECT_ID) {
- throw new Error("Missing NEXT_PUBLIC_EASYBLOCKS_PROJECT_ID");
- }
-
- const documents = await apiClient.documents.getDocuments({
- projectId: process.env.NEXT_PUBLIC_EASYBLOCKS_PROJECT_ID,
- });
-
- return (
-
-
-
- {documents.length >= 0 && (
-
-
-
- )}
-
-
- {[...documents]
- .sort(
- (a, b) =>
- new Date(b.updated_at).getTime() -
- new Date(a.updated_at).getTime()
- )
- .map((document) => {
- return (
-
-
-
-
-
{document.title}
-
- {formatRelative(
- new Date(document.updated_at),
- new Date(),
- {
- weekStartsOn: 1,
- }
- )}
-
-
-
-
-
-
-
- );
- })}
-
- {documents.length === 0 && (
-
-
No documents
-
- )}
-
-
-
- );
-}
-
-export const revalidate = 0;
diff --git a/apps/example/src/app/shopstory-canvas/layout.tsx b/apps/example/src/app/shopstory-canvas/layout.tsx
new file mode 100644
index 00000000..30f09b07
--- /dev/null
+++ b/apps/example/src/app/shopstory-canvas/layout.tsx
@@ -0,0 +1,11 @@
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/example/src/app/shopstory-canvas/page.tsx b/apps/example/src/app/shopstory-canvas/page.tsx
index 0bbc0faa..1623309c 100644
--- a/apps/example/src/app/shopstory-canvas/page.tsx
+++ b/apps/example/src/app/shopstory-canvas/page.tsx
@@ -1,39 +1,12 @@
"use client";
-import {
- builtinEditableComponents,
- builtinEditableComponentsDefinitions,
-} from "@easyblocks/editable-components";
-import { Canvas, ShopstoryProvider } from "@easyblocks/react";
-import { accessToken } from "../lib/apiClient";
-
+import { Canvas } from "@easyblocks/react";
+import { shopstoryConfig } from "@/app/shopstory/shopstory.config";
+import { AcmeShopstoryProvider } from "@/app/shopstory/AcmeShopstoryProvider";
export default function ShopstoryCanvas() {
return (
-
-
-
+
+
+
);
}
diff --git a/apps/example/src/app/shopstory/AcmeShopstoryProvider.tsx b/apps/example/src/app/shopstory/AcmeShopstoryProvider.tsx
new file mode 100644
index 00000000..7088aac9
--- /dev/null
+++ b/apps/example/src/app/shopstory/AcmeShopstoryProvider.tsx
@@ -0,0 +1,12 @@
+import { builtinEditableComponents } from "@easyblocks/editable-components";
+import { ShopstoryProvider } from "@easyblocks/react";
+
+export const AcmeShopstoryProvider: React.FC<{
+ children?: React.ReactNode;
+}> = ({ children }) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/apps/example/src/app/shopstory/shopstory.config.ts b/apps/example/src/app/shopstory/shopstory.config.ts
new file mode 100644
index 00000000..607521b1
--- /dev/null
+++ b/apps/example/src/app/shopstory/shopstory.config.ts
@@ -0,0 +1,25 @@
+import { Config } from "@easyblocks/core";
+import { builtinEditableComponentsDefinitions } from "@easyblocks/editable-components";
+
+export const shopstoryConfig: Config = {
+ accessToken: process.env.NEXT_PUBLIC_EASYBLOCKS_ACCESS_TOKEN,
+ locales: [
+ {
+ code: "en-US",
+ isDefault: true,
+ },
+ {
+ code: "de-DE",
+ fallback: "en-US",
+ },
+ ],
+ rootContainers: {
+ content: {
+ defaultConfig: {
+ _template: "$RootSections",
+ data: [],
+ },
+ },
+ },
+ components: builtinEditableComponentsDefinitions,
+};
diff --git a/apps/example/src/data/MockData/MockDataService.ts b/apps/example/src/data/MockData/MockDataService.ts
new file mode 100644
index 00000000..52668b28
--- /dev/null
+++ b/apps/example/src/data/MockData/MockDataService.ts
@@ -0,0 +1,85 @@
+export interface Entry {
+ id: string;
+ name: string;
+ description: string;
+ createdAt: Date;
+ updatedAt: Date;
+ page?: any;
+}
+
+const TEN_DAYS_IN_SECONDS = 10 * 24 * 60 * 60 * 1000;
+
+export class MockDataService {
+ private readonly storageKey = "entries";
+ private readonly mockEntries: Entry[] = [
+ {
+ id: "84b180c9-4a47-4ecf-bc59-21c2069060e3",
+ name: "Mock Entry 1",
+ description: "This is a mock entry",
+ createdAt: new Date(new Date().getTime() - 2 * TEN_DAYS_IN_SECONDS),
+ updatedAt: new Date(new Date().getTime() - 2 * TEN_DAYS_IN_SECONDS),
+ },
+ {
+ id: "d9b657e3-2b17-47ae-8450-3d7f1ce47ce3",
+ name: "Mock Entry 2",
+ description: "This is another mock entry",
+ createdAt: new Date(new Date().getTime() - TEN_DAYS_IN_SECONDS),
+ updatedAt: new Date(new Date().getTime() - TEN_DAYS_IN_SECONDS),
+ },
+ {
+ id: "b85bb2dc-9769-44b7-ac53-93e0ca9134eb",
+ name: "Mock Entry 3",
+ description: "This is yet another mock entry",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ ];
+
+ constructor() {
+ this.initStorage();
+ }
+
+ private initStorage() {
+ if (!localStorage.getItem(this.storageKey)) {
+ localStorage.setItem(this.storageKey, JSON.stringify(this.mockEntries));
+ }
+ }
+
+ public getEntries(): Entry[] {
+ const entriesString = localStorage.getItem(this.storageKey);
+ if (entriesString === null) {
+ throw new Error("unreachable");
+ }
+ return JSON.parse(entriesString).map((entry: any) => ({
+ ...entry,
+ createdAt: new Date(entry.createdAt),
+ updatedAt: new Date(entry.updatedAt),
+ }));
+ }
+
+ public getEntryById(id: string): Entry | undefined {
+ const entries = this.getEntries();
+ return entries.find((entry) => entry.id === id);
+ }
+
+ public addEntry(entry: Entry) {
+ const currentEntries = this.getEntries();
+ if (currentEntries) {
+ currentEntries.push(entry);
+ localStorage.setItem(this.storageKey, JSON.stringify(currentEntries));
+ }
+ }
+
+ public updateEntry(entry: Entry) {
+ let entries = this.getEntries();
+ if (entries) {
+ entries = entries.map((e) => {
+ if (e.id === entry.id) {
+ return entry;
+ }
+ return e;
+ });
+ localStorage.setItem(this.storageKey, JSON.stringify(entries));
+ }
+ }
+}
diff --git a/apps/example/src/data/MockData/useMockService.ts b/apps/example/src/data/MockData/useMockService.ts
new file mode 100644
index 00000000..ef80e753
--- /dev/null
+++ b/apps/example/src/data/MockData/useMockService.ts
@@ -0,0 +1,12 @@
+import { useEffect, useState } from "react";
+import { MockDataService } from "@/data/MockData/MockDataService";
+
+export function useMockService() {
+ const [service, setService] = useState(null);
+
+ useEffect(() => {
+ setService(new MockDataService());
+ }, []);
+
+ return service;
+}
diff --git a/apps/example/src/pages/documents/[id]/index.tsx b/apps/example/src/pages/documents/[id]/index.tsx
index a48b5341..30ae8def 100644
--- a/apps/example/src/pages/documents/[id]/index.tsx
+++ b/apps/example/src/pages/documents/[id]/index.tsx
@@ -1,81 +1,3 @@
-import { Metadata, RenderableContent, ShopstoryClient } from "@easyblocks/core";
-import {
- builtinClientOnlyEditableComponents,
- builtinEditableComponentsDefinitions,
-} from "@easyblocks/editable-components";
-import {
- Shopstory,
- ShopstoryMetadataProvider,
- ShopstoryProvider,
-} from "@easyblocks/react";
-import { GetServerSideProps, InferGetServerSidePropsType } from "next";
-import { createApiClient } from "../../../app/lib/apiClient";
-
-function DocumentPage({
- content,
- meta,
-}: InferGetServerSidePropsType) {
- return (
-
-
-
-
-
- );
+export default function DocumentsPage() {
+ return Nothing
;
}
-
-export default DocumentPage;
-
-export const getServerSideProps: GetServerSideProps<
- { content: RenderableContent; meta: Metadata },
- { id: string }
-> = async (context) => {
- const { id } = context.params ?? {};
-
- const apiClient = createApiClient();
-
- if (!id) {
- return {
- notFound: true,
- };
- }
-
- const document = await apiClient.documents.getDocumentById({
- projectId: "89ed48c6-dc0b-4936-9a97-4eb791396853",
- documentId: id,
- });
-
- if (document === null) {
- return {
- notFound: true,
- };
- }
-
- const shopstoryClient = new ShopstoryClient(
- {
- components: builtinEditableComponentsDefinitions,
- rootContainers: {
- content: {
- defaultConfig: {
- _template: "$RootSections",
- data: [],
- },
- },
- },
- accessToken:
- process.env.NEXT_PUBLIC_ACCESS_TOKEN ??
- "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NjE5NTM2NzMsImV4cCI6MTY5MzQ4OTY3MywiYXVkIjoiYXV0aGVudGljYXRlZCIsInN1YiI6IjEyMyIsImVtYWlsIjoibWljaGFsQHNob3BzdG9yeS5hcHAiLCJwcm9qZWN0X2lkIjoiODllZDQ4YzYtZGMwYi00OTM2LTlhOTctNGViNzkxMzk2ODUzIn0.3I9QMjhPkYHtzW19g-uIjattobPBiXEK0Fz4AwxEfQg",
- },
- { locale: "en-US" }
- );
-
- const content = shopstoryClient.add(document.config.config);
- const meta = await shopstoryClient.build();
-
- return {
- props: {
- content,
- meta,
- },
- };
-};
diff --git a/apps/example/src/utils/generatePreviewUrl.ts b/apps/example/src/utils/generatePreviewUrl.ts
new file mode 100644
index 00000000..6545ff7d
--- /dev/null
+++ b/apps/example/src/utils/generatePreviewUrl.ts
@@ -0,0 +1,39 @@
+export function generatePreviewUrl(params: {
+ id: string;
+ projectId: string;
+ version: number;
+ contextParams: Record;
+ accessToken: string;
+ canvasUrl: string;
+}): string {
+ const previewUrl = `${params.canvasUrl}?documentId=${params.id}&projectId=${
+ params.projectId
+ }&accessToken=${params.accessToken}&contextParams=${JSON.stringify(
+ params.contextParams
+ )}&preview=true&version=${params.version}`;
+
+ if (isLocalhost()) {
+ return "https://placehold.co/600x400";
+ }
+
+ const searchParams = new URLSearchParams();
+ searchParams.set("access_key", "yeKG1SmUryGzuw");
+ searchParams.set("url", previewUrl);
+ searchParams.set("cache", "true");
+ searchParams.set("format", "jpg");
+ searchParams.set("block_cookie_banners", "true");
+ searchParams.set("block_trackers", "true");
+ searchParams.set("device_scale_factor", "1");
+ searchParams.set("selector", "#__shopstory-container");
+
+ const screenshotPreviewUrl = `https://api.screenshotone.com/take?${searchParams.toString()}`;
+ return screenshotPreviewUrl;
+}
+
+function isLocalhost() {
+ return (
+ location.hostname === "localhost" ||
+ location.hostname === "127.0.0.1" ||
+ location.hostname === ""
+ );
+}
diff --git a/packages/core/src/buildPreview.ts b/packages/core/src/buildPreview.ts
index 267f31c9..433c3026 100644
--- a/packages/core/src/buildPreview.ts
+++ b/packages/core/src/buildPreview.ts
@@ -3,20 +3,24 @@ import { ShopstoryClient } from "./ShopstoryClient";
import { Config, ContextParams } from "./types";
export async function buildPreview(
- configId: string,
+ documentId: string,
+ projectId: string,
width: number | undefined,
widthAuto: boolean | undefined,
accessToken: string,
config: Config,
contextParams: ContextParams
) {
- const response = await fetch(`${getAppUrlRoot()}/api/configs/${configId}`, {
- method: "GET",
- headers: {
- Accept: "application/json",
- "x-shopstory-access-token": accessToken,
- },
- });
+ const response = await fetch(
+ `${getAppUrlRoot()}/api/projects/${projectId}/documents/${documentId}`,
+ {
+ method: "GET",
+ headers: {
+ Accept: "application/json",
+ "x-shopstory-access-token": accessToken,
+ },
+ }
+ );
const data = await response.json();
@@ -26,7 +30,7 @@ export async function buildPreview(
_template: "$ComponentContainer",
widthAuto: widthAuto ?? false,
width: width ?? 5000,
- Component: [data.config],
+ Component: [data.config.config],
});
const meta = await client.build();
diff --git a/packages/editor/src/Editor.tsx b/packages/editor/src/Editor.tsx
index c19d32fb..286e7e67 100644
--- a/packages/editor/src/Editor.tsx
+++ b/packages/editor/src/Editor.tsx
@@ -937,18 +937,11 @@ const EditorContent = ({
handleSetBreakpoint(newBreakpointIndex);
},
actions,
- save: async (localisedDocument, externals) => {
- if (props.save) {
- await props.save(localisedDocument, externals);
- } else {
- window.postMessage({
- type: "@shopstory-editor/content-saved",
- payload: {
- localisedDocument,
- references: externals,
- },
- });
- }
+ save: async (documentData) => {
+ window.postMessage({
+ type: "@easyblocks/content-saved",
+ document: documentData,
+ });
},
text: undefined,
locales: props.locales,
@@ -1138,12 +1131,15 @@ const EditorContent = ({
saveNow().finally(() => {
setDataSaverOverlayOpen(false);
+ window.postMessage(
+ {
+ type: "@easyblocks/closed",
+ },
+ "*"
+ );
+
if (props.onClose) {
props.onClose();
- } else {
- window.postMessage({
- type: "@shopstory-editor/closed",
- });
}
if (isDemoProject) {
diff --git a/packages/editor/src/EditorContext.ts b/packages/editor/src/EditorContext.ts
index 8680136b..922e190b 100644
--- a/packages/editor/src/EditorContext.ts
+++ b/packages/editor/src/EditorContext.ts
@@ -8,8 +8,6 @@ import { CompilationCache } from "@easyblocks/compiler";
import {
CompiledComponentConfig,
ConfigComponent,
- ExternalReference,
- LocalisedDocument,
LocalizedText,
Resource,
ResourceDefinition,
@@ -27,10 +25,12 @@ export type EditorContextType = BaseEditorContextType & {
isEditing?: boolean;
actions: ActionsType;
text?: ResourceDefinition & TextSyncers;
- save: (
- localisedDocument: LocalisedDocument,
- externals: ExternalReference[]
- ) => Promise;
+ save: (document: {
+ id: string;
+ version: number;
+ updatedAt: number;
+ projectId: string;
+ }) => Promise;
compiledComponentConfig?: CompiledComponentConfig;
configAfterAuto?: ConfigComponent;
variantsManager?: VariantsManager;
diff --git a/packages/editor/src/launchEditor.tsx b/packages/editor/src/launchEditor.tsx
index b1d328e7..64b27386 100644
--- a/packages/editor/src/launchEditor.tsx
+++ b/packages/editor/src/launchEditor.tsx
@@ -64,6 +64,7 @@ export function launchEditor(props: EditorLauncherProps) {
editorSearchParams.contextParams ??
props.contextParams ??
raiseError(`Missing "contextParams" value.`);
+
const rootContainer =
editorSearchParams.rootContainer ?? props.rootContainer ?? props.mode;
const mode = editorSearchParams.mode ?? "playground";
diff --git a/packages/editor/src/useDataSaver.ts b/packages/editor/src/useDataSaver.ts
index dbdf84e7..ed646910 100644
--- a/packages/editor/src/useDataSaver.ts
+++ b/packages/editor/src/useDataSaver.ts
@@ -388,31 +388,45 @@ export function useDataSaver(
}
}
- const configAfterSplit = splitConfigIntoSingleLocaleConfigs(
- configToSaveWithLocalisedFlag,
- editorContext.locales
- );
- const localisedDocument: LocalisedDocument = {};
- const previewData = getPreviewData(
- configToSaveWithLocalisedFlag,
- editorContext.rootContainer
- );
-
- editorContext.locales.forEach((locale) => {
- localisedDocument[locale.code] = {
- documentId: editorContext.isPlayground
- ? "playground-document"
- : remoteDocument.current!.id,
- config: configAfterSplit[locale.code],
- preview: previewData,
- projectId: editorContext.project
- ? editorContext.project.id
- : "playground",
- rootContainer: editorContext.rootContainer,
- };
- });
-
- await editorContext.save(localisedDocument, externalReferences);
+ const documentData = {
+ id: editorContext.isPlayground
+ ? "playground-document"
+ : remoteDocument.current!.id,
+ version: editorContext.isPlayground
+ ? 0
+ : remoteDocument.current!.version,
+ updatedAt: new Date().getTime(),
+ projectId: editorContext.project
+ ? editorContext.project.id
+ : "playground",
+ };
+
+ // const configAfterSplit = splitConfigIntoSingleLocaleConfigs(
+ // configToSaveWithLocalisedFlag,
+ // editorContext.locales
+ // );
+ //
+ // const localisedDocument: LocalisedDocument = {};
+ // const previewData = getPreviewData(
+ // configToSaveWithLocalisedFlag,
+ // editorContext.rootContainer
+ // );
+ //
+ // editorContext.locales.forEach((locale) => {
+ // localisedDocument[locale.code] = {
+ // documentId: editorContext.isPlayground
+ // ? "playground-document"
+ // : remoteDocument.current!.id,
+ // config: configAfterSplit[locale.code],
+ // preview: previewData,
+ // projectId: editorContext.project
+ // ? editorContext.project.id
+ // : "playground",
+ // rootContainer: editorContext.rootContainer,
+ // };
+ // });
+
+ await editorContext.save(documentData);
if (Object.keys(externalsMap).length > 0) {
const newFormValues = updateConfigExternals(
diff --git a/packages/react/src/Canvas.tsx b/packages/react/src/Canvas.tsx
index e76d1243..9a888efa 100644
--- a/packages/react/src/Canvas.tsx
+++ b/packages/react/src/Canvas.tsx
@@ -20,7 +20,7 @@ export function Canvas(props: CanvasProps) {
};
useEffect(() => {
- if (parseQueryParams().configId) {
+ if (parseQueryParams().preview) {
setSelectedWindow("preview");
return;
}
diff --git a/packages/react/src/PreviewRenderer.tsx b/packages/react/src/PreviewRenderer.tsx
index 559e2c2d..6287c023 100644
--- a/packages/react/src/PreviewRenderer.tsx
+++ b/packages/react/src/PreviewRenderer.tsx
@@ -16,10 +16,16 @@ export const PreviewRenderer: React.FC = ({
useEffect(() => {
import("@easyblocks/core").then(({ buildPreview }) => {
- const { configId, accessToken, width, widthAuto, contextParams } =
- parseQueryParams();
+ const {
+ documentId,
+ projectId,
+ accessToken,
+ width,
+ widthAuto,
+ contextParams,
+ } = parseQueryParams();
- if (!configId) {
+ if (!documentId || !projectId) {
throw new Error("unreachable");
}
@@ -32,7 +38,8 @@ export const PreviewRenderer: React.FC = ({
}
buildPreview(
- configId,
+ documentId,
+ projectId,
width,
widthAuto,
accessToken,
diff --git a/packages/react/src/parseQueryParams.ts b/packages/react/src/parseQueryParams.ts
index 22f691d0..693fbf5f 100644
--- a/packages/react/src/parseQueryParams.ts
+++ b/packages/react/src/parseQueryParams.ts
@@ -1,19 +1,22 @@
import type { ContextParams } from "@easyblocks/core";
export function parseQueryParams(): {
- configId: undefined | string;
+ documentId: undefined | string;
+ projectId: undefined | string;
accessToken: undefined | string;
mode: "playground" | "app";
widthAuto: boolean;
width: number | undefined;
contextParams: ContextParams | undefined;
+ preview: boolean;
} {
const urlSearchParams = new URLSearchParams(window.location.search);
const queryParams = Object.fromEntries(urlSearchParams.entries());
const contextParams = parseContextParams(queryParams.contextParams);
return {
- configId: queryParams.configId,
+ documentId: queryParams.documentId,
+ projectId: queryParams.projectId,
/**
* We need shopstoryAccessToken for non-cloud versions - they rely on this query param.
* It can be removed only when all clients move to cloud based version and 1.0.0 SDK.
@@ -23,6 +26,7 @@ export function parseQueryParams(): {
widthAuto: queryParams.widthAuto === "true" ? true : false,
width: queryParams.width ? parseInt(queryParams.width) : undefined,
contextParams,
+ preview: queryParams.preview === "true",
};
}