From 969330bab6653148c1b13688903080eeb82e3def Mon Sep 17 00:00:00 2001 From: Todd Olson Date: Thu, 19 Mar 2026 14:01:46 -0400 Subject: [PATCH] NOVUS-357 Add in-app Markdown report viewer NOVUS-357 Adds a "View Report" button on the Dashboard that opens a styled preview modal for OKR markdown reports. Users can view a pretty-rendered report with progress bars or export the .md file directly from the modal. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../common/MarkdownPreviewModal.tsx | 122 ++++++++++++++++++ src/pages/Dashboard.tsx | 49 ++++++- 2 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 src/components/common/MarkdownPreviewModal.tsx diff --git a/src/components/common/MarkdownPreviewModal.tsx b/src/components/common/MarkdownPreviewModal.tsx new file mode 100644 index 0000000..ece8a8e --- /dev/null +++ b/src/components/common/MarkdownPreviewModal.tsx @@ -0,0 +1,122 @@ +import { useEffect, type ReactNode } from 'react'; + +interface MarkdownPreviewModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + markdown: string; + onExport: () => void; +} + +function renderMarkdown(markdown: string): ReactNode[] { + const lines = markdown.split('\n'); + const elements: ReactNode[] = []; + let key = 0; + + for (const line of lines) { + if (line.startsWith('### ')) { + elements.push( +

+ {line.slice(4)} +

+ ); + } else if (line.startsWith('## ')) { + elements.push( +

+ {line.slice(3)} +

+ ); + } else if (line.startsWith('# ')) { + elements.push( +

+ {line.slice(2)} +

+ ); + } else if (line.startsWith('- ')) { + const text = line.slice(2); + const percentMatch = text.match(/\((\d+)%\)/); + const percent = percentMatch ? parseInt(percentMatch[1]) : null; + + elements.push( +
+ + {text} + {percent !== null && ( +
+
+
= 100 ? 'bg-green-500' : percent >= 70 ? 'bg-blue-500' : percent >= 40 ? 'bg-yellow-500' : 'bg-red-400' + }`} + style={{ width: `${Math.min(percent, 100)}%` }} + /> +
+
+ )} +
+ ); + } else if (line.trim() === '') { + elements.push(
); + } + } + + return elements; +} + +export function MarkdownPreviewModal({ isOpen, onClose, title, markdown, onExport }: MarkdownPreviewModalProps) { + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + + if (isOpen) { + document.addEventListener('keydown', handleEscape); + document.body.style.overflow = 'hidden'; + } + + return () => { + document.removeEventListener('keydown', handleEscape); + document.body.style.overflow = 'unset'; + }; + }, [isOpen, onClose]); + + if (!isOpen) return null; + + return ( +
+
+
+
+
+

{title}

+
+ + +
+
+
+ {renderMarkdown(markdown)} +
+
+
+
+ ); +} diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 076b8eb..1a71019 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,8 +1,11 @@ +import { useState } from 'react'; import { useOKR } from '../context/OKRContext'; import { ProgressBar } from '../components/common/ProgressBar'; +import { MarkdownPreviewModal } from '../components/common/MarkdownPreviewModal'; export function Dashboard() { const { objectives, selectedQuarter } = useOKR(); + const [showPreview, setShowPreview] = useState(false); const quarterObjectives = objectives.filter((o) => o.quarter === selectedQuarter); @@ -66,7 +69,7 @@ export function Dashboard() { }, ]; - const exportToMarkdown = () => { + const generateMarkdown = () => { let markdown = `# OKRs for ${selectedQuarter}\n\n`; const sections = [ @@ -91,6 +94,12 @@ export function Dashboard() { } }); + return markdown; + }; + + const exportToMarkdown = () => { + const markdown = generateMarkdown(); + if (window.pendo) { window.pendo.track("okr_exported", { quarter: selectedQuarter, @@ -138,12 +147,27 @@ export function Dashboard() {

Overview for {selectedQuarter}

{quarterObjectives.length > 0 && ( - +
+ + +
)}
@@ -198,6 +222,17 @@ export function Dashboard() {

)} + + setShowPreview(false)} + title={`OKR Report - ${selectedQuarter}`} + markdown={generateMarkdown()} + onExport={() => { + exportToMarkdown(); + setShowPreview(false); + }} + /> ); }