From b92962b64ac17ffb9dcd22ed5b45dc623e26737c Mon Sep 17 00:00:00 2001 From: recursive-0 Date: Mon, 27 Oct 2025 11:32:07 +0530 Subject: [PATCH 1/2] add markdown export functionality --- src/features/storymap-preview.tsx | 23 +++++-- src/lib/utils.ts | 109 +++++++++++++++++++++++++++++- 2 files changed, 126 insertions(+), 6 deletions(-) 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..eea183f 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,111 @@ -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 `![${alt}](${url})${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 = (b: StorymapBlocks, level = 2): string => { + switch (b.type) { + case "text": + return b.payload.content + "\n"; + case "image": + return mdForImage(b.payload) + "\n"; + case "map": + return mdForMap(b.payload) + "\n"; + case "cover": { + const coverData = b.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}

` : "", + `
`, + `
`, + "" + ].join("\n"); + } + + return [ + "", + ...b.payload.cover_blocks.map(cb => mdForBlock(cb, level)), + "", + "", + ].join("\n"); + } + case "narrative": { + const title = b.payload.narrative_title ?? "Narrative"; + const h = "#".repeat(Math.min(level, 6)); + return [`${h} ${title}\n`, ...b.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(b.payload.map_config, "sidecar-map")); + b.payload.cards.forEach((card, i) => { + const hh = "#".repeat(Math.min(level + 1, 6)); + parts.push(`${hh} Card ${i + 1}\n\n`); + parts.push(card.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); +} From 0c019bdd443f836c4ac26481b1295b78ff865384 Mon Sep 17 00:00:00 2001 From: recursive-0 Date: Mon, 27 Oct 2025 11:49:54 +0530 Subject: [PATCH 2/2] clean up --- src/lib/utils.ts | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index eea183f..3e07eb4 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -29,51 +29,58 @@ export const mdForMap = (p: MapPayload, title?: string) => { ].join("\n"); }; -export const mdForBlock = (b: StorymapBlocks, level = 2): string => { - switch (b.type) { +export const mdForBlock = (presentationBlock: StorymapBlocks, level = 2): string => { + if (!presentationBlock.type) { + return `\n`; + } + switch (presentationBlock.type) { case "text": - return b.payload.content + "\n"; + return presentationBlock.payload.content + "\n"; case "image": - return mdForImage(b.payload) + "\n"; + return mdForImage(presentationBlock.payload) + "\n"; case "map": - return mdForMap(b.payload) + "\n"; + return mdForMap(presentationBlock.payload) + "\n"; case "cover": { - const coverData = b.payload; + 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}

` : "", + `
`, + `
`, + `
`, + `
`, + textBlock?.content ? `
${textBlock.content.replace(/^#\s*/, '')}
` : "", + `
`, `
`, `
`, "" ].join("\n"); } + // Fallback to individual blocks if no background image return [ "", - ...b.payload.cover_blocks.map(cb => mdForBlock(cb, level)), + ...presentationBlock.payload.cover_blocks.map(cb => mdForBlock(cb, level)), "", "", ].join("\n"); } case "narrative": { - const title = b.payload.narrative_title ?? "Narrative"; + const title = presentationBlock.payload.narrative_title ?? "Narrative"; const h = "#".repeat(Math.min(level, 6)); - return [`${h} ${title}\n`, ...b.payload.narrative_blocks.map(nb => mdForBlock(nb, level + 1))].join(""); + 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(b.payload.map_config, "sidecar-map")); - b.payload.cards.forEach((card, i) => { + 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(card.payload.content + "\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`); @@ -82,7 +89,7 @@ export const mdForBlock = (b: StorymapBlocks, level = 2): string => { return parts.join(""); } default: - return `\n`; + return `\n`; } };