-
Notifications
You must be signed in to change notification settings - Fork 0
NOVUS-357 Add in-app Markdown report viewer #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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( | ||
| <h3 key={key++} className="text-lg font-semibold text-gray-800 mt-4 mb-1"> | ||
| {line.slice(4)} | ||
| </h3> | ||
| ); | ||
| } else if (line.startsWith('## ')) { | ||
| elements.push( | ||
| <h2 key={key++} className="text-xl font-bold text-gray-900 mt-6 mb-2 pb-1 border-b border-gray-200"> | ||
| {line.slice(3)} | ||
| </h2> | ||
| ); | ||
| } else if (line.startsWith('# ')) { | ||
| elements.push( | ||
| <h1 key={key++} className="text-2xl font-bold text-gray-900 mb-4"> | ||
| {line.slice(2)} | ||
| </h1> | ||
| ); | ||
| } else if (line.startsWith('- ')) { | ||
| const text = line.slice(2); | ||
| const percentMatch = text.match(/\((\d+)%\)/); | ||
| const percent = percentMatch ? parseInt(percentMatch[1]) : null; | ||
|
|
||
| elements.push( | ||
| <div key={key++} className="flex items-center gap-3 py-1.5 pl-4"> | ||
| <span className="text-gray-400 text-sm">•</span> | ||
| <span className="text-sm text-gray-700 flex-1">{text}</span> | ||
| {percent !== null && ( | ||
| <div className="flex items-center gap-2 shrink-0"> | ||
| <div className="w-20 h-2 bg-gray-200 rounded-full overflow-hidden"> | ||
| <div | ||
| className={`h-full rounded-full transition-all ${ | ||
| percent >= 100 ? 'bg-green-500' : percent >= 70 ? 'bg-blue-500' : percent >= 40 ? 'bg-yellow-500' : 'bg-red-400' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [warning] The modal uses |
||
| }`} | ||
| style={{ width: `${Math.min(percent, 100)}%` }} | ||
| /> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [warning] The modal progress bars use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [warning] The modal progress bar uses |
||
| </div> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| } else if (line.trim() === '') { | ||
| elements.push(<div key={key++} className="h-1" />); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [warning] Plain text lines are silently dropped — the renderer only handles |
||
| } | ||
|
|
||
| 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 ( | ||
| <div className="fixed inset-0 z-50 overflow-y-auto"> | ||
| <div className="flex min-h-full items-center justify-center p-4"> | ||
| <div | ||
| className="fixed inset-0 bg-black/50 transition-opacity" | ||
| onClick={onClose} | ||
| /> | ||
| <div className="relative bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[80vh] flex flex-col transform transition-all"> | ||
| <div className="flex items-center justify-between p-6 pb-3 border-b border-gray-100"> | ||
| <h2 className="text-xl font-semibold text-gray-900">{title}</h2> | ||
| <div className="flex items-center gap-2"> | ||
| <button | ||
| onClick={onExport} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [warning] A high-priority Pendo signal flagged that the existing Export button on the Dashboard has dead-click frustration — "session oFkGVZZwQykbocLM shows the Export button triggers dead click frustration within the first second of the session, suggesting the button may lack immediate visual feedback on click" (Export to Markdown: 11% adoption, 3 events/2 visitors in 30 days). This new modal Export button has the same pattern: clicking it silently triggers a download with no loading state, success toast, or confirmation. The root frustration isn't being addressed — consider adding a brief visual confirmation (e.g., button label briefly changes to "Exported!") to signal the action completed. |
||
| className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors" | ||
| > | ||
| <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> | ||
| </svg> | ||
| Export | ||
| </button> | ||
| <button | ||
| onClick={onClose} | ||
| className="text-gray-400 hover:text-gray-600 transition-colors p-1" | ||
| > | ||
| <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> | ||
| </svg> | ||
| </button> | ||
| </div> | ||
| </div> | ||
| <div className="p-6 overflow-y-auto"> | ||
| {renderMarkdown(markdown)} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() { | |
| <p className="text-gray-600 mt-1">Overview for {selectedQuarter}</p> | ||
| </div> | ||
| {quarterObjectives.length > 0 && ( | ||
| <button | ||
| onClick={exportToMarkdown} | ||
| className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors" | ||
| > | ||
| Export to Markdown | ||
| </button> | ||
| <div className="flex items-center gap-2"> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [warning] The |
||
| <button | ||
| onClick={() => setShowPreview(true)} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [warning] Export adoption is at 10% (2 of 20 accounts), well below the 25% goal, and is flagged as a high-priority signal. The |
||
| className="inline-flex items-center gap-2 px-4 py-2 bg-white text-gray-700 text-sm font-medium rounded-lg border border-gray-300 hover:bg-gray-50 transition-colors" | ||
| > | ||
| <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> | ||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> | ||
| </svg> | ||
| View Report | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [warning] There are now two ways to trigger the exact same export from the same screen — this "Export .md" button and the "Export" button inside the modal. The modal was presumably introduced to let users preview before exporting, but keeping a direct export button alongside it undercuts that intent and creates ambiguity: is "View Report" a prerequisite step, or a parallel path? Consider removing this direct export button so the flow is clear: View Report → Export. |
||
| </button> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [info] Export adoption is well below target — 10% of visitors (2 of 20 accounts, 3 total clicks) over the last 30 days vs. a 25% account goal (active Pendo signal). This PR keeps the direct "Export .md" button in the header and adds a second export path inside the modal, splitting the funnel in two. If the preview modal is intended to build confidence and drive export adoption, consider removing the redundant top-level export button and routing all exports through View Report → Export — it creates a cleaner mental model and makes it easier to measure whether the modal is actually moving the adoption needle. |
||
| <button | ||
| onClick={exportToMarkdown} | ||
| className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [info] A high-priority Pendo signal (2026-03-14) flagged the Export button as a dead-click source — session oFkGVZZwQykbocLM recorded dead clicks on it within the first second, with only 11.1% adoption (2 visitors, 3 clicks) against a 25% goal. The new "View Report" modal is a good step toward making the feature feel more substantial, but the direct "Export .md" button still fires a silent file download with no visual acknowledgment. A brief toast ("Report downloaded") or a momentary button state change would close the feedback loop that's currently generating the dead-click signal. |
||
| > | ||
| <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> | ||
| </svg> | ||
| Export .md | ||
| </button> | ||
| </div> | ||
| )} | ||
| </div> | ||
|
|
||
|
|
@@ -198,6 +222,17 @@ export function Dashboard() { | |
| </p> | ||
| </div> | ||
| )} | ||
|
|
||
| <MarkdownPreviewModal | ||
| isOpen={showPreview} | ||
| onClose={() => setShowPreview(false)} | ||
| title={`OKR Report - ${selectedQuarter}`} | ||
| markdown={generateMarkdown()} | ||
| onExport={() => { | ||
| exportToMarkdown(); | ||
| setShowPreview(false); | ||
| }} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[warning] The progress bar uses
bg-blue-500for 70–99% — but everywhere else in the product (OKR Management pages, Dashboard progress bars) the color vocabulary is explicitly red < 40%, yellow 40–70%, green > 70%. A user who sees a 75% KR as green on the Company OKRs page will see the same KR as blue in this modal, which will read as a different status. Changebg-blue-500tobg-green-500here to match the established scale, and drop the separate 100% green band (or keep it asbg-emerald-500if you want to distinguish "complete").