diff --git a/src/features/storymap-preview.tsx b/src/features/storymap-preview.tsx
index d93ad72..a1dd6d1 100644
--- a/src/features/storymap-preview.tsx
+++ b/src/features/storymap-preview.tsx
@@ -1,6 +1,9 @@
import { createSession } from "@/api/create-session";
+import { Button } from "@/components/ui/button";
+import { downloadMarkdown, storymapToMarkdown } from "@/lib/utils";
import { useUserStore } from "@/store/user-store";
import { DummyTemplate } from "@/tests/dummy-templates";
+import type { StorymapTemplate } from "@/types/storymap.types";
import { useMutation } from "@tanstack/react-query";
import { useEffect } from "react";
import EmptySessionFallback from "./loading-workspace";
@@ -39,6 +42,12 @@ export const StorymapPreview = () => {
const { mutate: callCreateSession } = createSessionMutation;
+ const exportContentAsMarkdown = () => {
+ if (!storymapContent) return;
+ const markdown = storymapToMarkdown(storymapContent as StorymapTemplate);
+ downloadMarkdown("storymap.md", markdown);
+ };
+
useEffect(() => {
if (!sessionId) {
callCreateSession();
@@ -52,10 +61,16 @@ export const StorymapPreview = () => {
}
return (
-
- {storymapContent && (
-
- )}
+
+
+ {storymapContent?.presentation_title || "Presentation Title here..."}
+
+
+
+ {storymapContent && (
+
+ )}
+
);
};
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index bd0c391..3e07eb4 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,6 +1,118 @@
-import { clsx, type ClassValue } from "clsx"
-import { twMerge } from "tailwind-merge"
+import type { ImagePayload, MapPayload, StorymapBlocks, StorymapTemplate } from "@/types/storymap.types";
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+
+
+
+export const mdForImage = (p: ImagePayload) => {
+ const alt = p.source.alt ?? "";
+ const url = p.source.url ?? "";
+ const cap = p.caption ? `\n*${p.caption}*` : "";
+ return `${cap}\n`;
+};
+
+export const mdForMap = (p: MapPayload, title?: string) => {
+ const ims = p.initial_map_state ?? { latitude: "", longitude: "", zoom: "" };
+ const layers = (p.layers ?? [])
+ .map(l => `{id:"${l.layer_id}",visible:${String(!!l.visible)}}`)
+ .join(", ");
+ const style = p.base_style ?? "";
+ return [
+ "```storymap",
+ `{map title="${title ?? ""}" lat=${ims.latitude} lng=${ims.longitude} zoom=${ims.zoom} style="${style}" layers=[${layers}]}`,
+ "```",
+ "",
+ ].join("\n");
+};
+
+export const mdForBlock = (presentationBlock: StorymapBlocks, level = 2): string => {
+ if (!presentationBlock.type) {
+ return `\n`;
+ }
+ switch (presentationBlock.type) {
+ case "text":
+ return presentationBlock.payload.content + "\n";
+ case "image":
+ return mdForImage(presentationBlock.payload) + "\n";
+ case "map":
+ return mdForMap(presentationBlock.payload) + "\n";
+ case "cover": {
+ const coverData = presentationBlock.payload;
+ const backgroundImage = coverData.cover_blocks.find(block => block.type === "image")?.payload;
+ const textBlock = coverData.cover_blocks.find(block => block.type === "text")?.payload;
+
+ if (backgroundImage?.source?.url) {
+ return [
+ `
`,
+ `
`,
+ `
`,
+ `
`,
+ textBlock?.content ? `
${textBlock.content.replace(/^#\s*/, '')}
` : "",
+ `
`,
+ `
`,
+ `
`,
+ ""
+ ].join("\n");
+ }
+
+ // Fallback to individual blocks if no background image
+ return [
+ "",
+ ...presentationBlock.payload.cover_blocks.map(cb => mdForBlock(cb, level)),
+ "",
+ "",
+ ].join("\n");
+ }
+ case "narrative": {
+ const title = presentationBlock.payload.narrative_title ?? "Narrative";
+ const h = "#".repeat(Math.min(level, 6));
+ return [`${h} ${title}\n`, ...presentationBlock.payload.narrative_blocks.map(nb => mdForBlock(nb, level + 1))].join("");
+ }
+ case "sidecar": {
+ const h = "#".repeat(Math.min(level, 6));
+ const parts: string[] = [];
+ parts.push(`${h} Sidecar\n\n`);
+ parts.push(mdForMap(presentationBlock.payload.map_config, "sidecar-map"));
+ presentationBlock.payload.cards.forEach((card, i) => {
+ const hh = "#".repeat(Math.min(level + 1, 6));
+ parts.push(`${hh} Card ${i + 1}\n\n`);
+ parts.push(presentationBlock.payload.cards[i].payload.content + "\n");
+ if (card.map_command?.type === "TOGGLE_LAYER") {
+ const { layer_id, visible } = card.map_command.payload;
+ parts.push(`> Map toggle: layer \`${layer_id}\` → visible=${visible}\n\n`);
+ }
+ });
+ return parts.join("");
+ }
+ default:
+ return `\n`;
+ }
+};
+
+export const storymapToMarkdown = (t: StorymapTemplate): string => {
+ const frontmatter = [
+ "---",
+ `Title: ${t.presentation_title}`,
+ "Exported: true",
+ "---",
+ "",
+ ].join("\n");
+ const title = `# ${t.presentation_title}\n\n`;
+ const body = t.presentation_blocks.map(b => mdForBlock(b, 2)).join("\n");
+ return frontmatter + title + body;
+}
+
+export const downloadMarkdown = (filename: string, md: string) => {
+ const blob = new Blob([md], { type: "text/markdown;charset=utf-8" });
+ const a = document.createElement("a");
+ a.href = URL.createObjectURL(blob);
+ a.download = filename.endsWith(".md") ? filename : `${filename}.md`;
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ URL.revokeObjectURL(a.href);
+}