Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions src/components/common/MarkdownPreviewModal.tsx
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">&#8226;</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'
Copy link
Copy Markdown

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-500 for 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. Change bg-blue-500 to bg-green-500 here to match the established scale, and drop the separate 100% green band (or keep it as bg-emerald-500 if you want to distinguish "complete").

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[warning] The modal uses bg-blue-500 for 70–99% progress, but the rest of the app uses green for >70% (the OKR management pages and Dashboard progress bars all follow red < 40%, yellow 40–70%, green > 70%). Users who open this report after viewing their OKRs will see the same objective — say, 75% complete — rendered green on the OKR page and blue in the modal, which contradicts the color coding they've internalized. Change bg-blue-500 to bg-green-500 to stay consistent with the existing semantic.

}`}
style={{ width: `${Math.min(percent, 100)}%` }}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[warning] The modal progress bars use bg-blue-500 for 70–99% completion, but the app's established color system uses green for >70% (as documented in OKR Management and visible in the existing ProgressBar component). Users who've learned "green = on track" in every other OKR view will misread blue as a neutral or unknown state in the report preview. Change the 70% threshold branch from bg-blue-500 to bg-green-500 to stay consistent.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[warning] The modal progress bar uses bg-blue-500 for 70–99% completion, but every other progress bar in the app (OKR Management pages) uses green for >70%. A user who sees a KR at 75% rendered green on the Team OKRs page will see the same KR colored blue in this modal — two different signals for the same state. Change bg-blue-500 to bg-green-500 to match the established red/yellow/green color system.

</div>
</div>
)}
</div>
);
} else if (line.trim() === '') {
elements.push(<div key={key++} className="h-1" />);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[warning] Plain text lines are silently dropped — the renderer only handles #, ##, ###, -, and empty lines, with no fallback. generateMarkdown() emits "No objectives in this category.\n" for empty sections, which is a plain text line and will disappear entirely in the preview. Users will see a blank gap where that message should appear with no indication content is missing. Add an else clause that renders unrecognized lines as a <p> element.

}

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}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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>
);
}
49 changes: 42 additions & 7 deletions src/pages/Dashboard.tsx
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);

Expand Down Expand Up @@ -66,7 +69,7 @@ export function Dashboard() {
},
];

const exportToMarkdown = () => {
const generateMarkdown = () => {
let markdown = `# OKRs for ${selectedQuarter}\n\n`;

const sections = [
Expand All @@ -91,6 +94,12 @@ export function Dashboard() {
}
});

return markdown;
};

const exportToMarkdown = () => {
const markdown = generateMarkdown();

if (window.pendo) {
window.pendo.track("okr_exported", {
quarter: selectedQuarter,
Expand Down Expand Up @@ -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">
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[warning] The okr_report_viewed track event exists in the system (defined to capture quarter + objective counts for adoption tracking) but is never fired here — only a Pendo click feature tag will fire. Export is currently at 11.1% account adoption against a 25% goal (signal from Mar 14–20, 2 of 18 accounts), and this modal is the explicit mechanism to close that gap. Without the track event, you can't measure whether the preview step actually increases export conversions. Add window.pendo?.track('okr_report_viewed', { quarter: selectedQuarter, ... }) alongside setShowPreview(true).

<button
onClick={() => setShowPreview(true)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 okr_report_viewed track event exists as an active artifact specifically to measure whether the preview path converts to exports — but this click handler only calls setShowPreview(true) with no window.pendo.track("okr_report_viewed", ...) call. Without it, you won't be able to tell how many users open the report vs. go on to export, making it impossible to evaluate whether this new flow moves the adoption needle.

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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>

Expand Down Expand Up @@ -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>
);
}