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 `![${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 = (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); +}