Skip to content
Merged
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
254 changes: 254 additions & 0 deletions website/src/components/CopyMarkdownButton/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import React, { useState, useRef, useEffect } from 'react';
import { useLocation } from '@docusaurus/router';
import styles from './styles.module.css';

const CHATGPT_PROMPT = `Read from {{document}} so I can ask you questions about it`
const CLAUDE_PROMPT = `Read from {{document}} so I can answer questions about it`

export default function CopyMarkdownButton({docId}) {
const [copied, setCopied] = useState(false);
const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [content, setContent] = useState(null);
const location = useLocation();
const dropdownRef = useRef(null);

useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);

const fetchMarkdown = async () => {
const mdUrl = `${window.location.origin}/${docId}.md`;

if (content) return content;
try {
const response = await fetch(mdUrl);
if (!response.ok) {
throw new Error('Failed to fetch markdown content');
}
const text = await response.text();
setContent(text);
return text;
} catch (error) {
console.error('Failed to fetch markdown:', error);
throw error;
}
};

const handleCopy = async () => {
try {
setLoading(true);
const text = await fetchMarkdown();
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
alert('Failed to copy markdown content. The .md file may not be available.');
} finally {
setLoading(false);
}
};

const handleOpenInChatGPT = async () => {
const browserUrl = `${window.location.origin}${location.pathname}`;
const prompt = CHATGPT_PROMPT.replace('{{document}}', browserUrl);
window.open(`https://chatgpt.com/?prompt=${prompt}`, '_blank');
};

const handleOpenInClaude = async () => {
const browserUrl = `${window.location.origin}${location.pathname}`;
const prompt = CLAUDE_PROMPT.replace('{{document}}', browserUrl);
window.open(`https://claude.ai/new?q=${prompt}`, '_blank');
};

const handleConnectMCPCursor = () => {
window.open(`cursor:mcp/install?%7B%22name%22%3A%22NEAR%20Docs%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fdocs.near.org%2Fmcp%22%7D`, '_blank');
setIsOpen(false);
};

const handleConnectMCPVSCode = () => {
window.open(`vscode:mcp/install?%7B%22name%22%3A%22NEAR%20Docs%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fdocs.near.org%2Fmcp%22%7D`, '_blank');
setIsOpen(false);
};

return (
<div className={styles.container} ref={dropdownRef}>
<div className={styles.buttonGroup}>
<button
className={styles.copyButton}
onClick={handleCopy}
disabled={loading}
title="Copy markdown content"
>
{loading ? (
<span className={styles.spinner} />
) : copied ? (
<>
<CheckIcon />
<span>Copied!</span>
</>
) : (
<>
<CopyIcon />
<span>Copy Markdown</span>
</>
)}
</button>
<button
className={styles.dropdownButton}
onClick={() => setIsOpen(!isOpen)}
disabled={loading}
title="More options"
>
<ChevronIcon isOpen={isOpen} />
</button>
</div>

{isOpen && (
<div className={styles.dropdown}>
<button
className={styles.menuItem}
onClick={handleOpenInChatGPT}
disabled={loading}
>
<ExternalLinkIcon />
<span>Open in ChatGPT</span>
</button>
<button
className={styles.menuItem}
onClick={handleOpenInClaude}
disabled={loading}
>
<ExternalLinkIcon />
<span>Open in Claude</span>
</button>
<button
className={styles.menuItem}
onClick={handleConnectMCPCursor}
disabled={loading}
>
<LinkIcon />
<span>Cursor: Connect MCP</span>
</button>
<button
className={styles.menuItem}
onClick={handleConnectMCPVSCode}
disabled={loading}
>
<LinkIcon />
<span>VSCode: Connect MCP</span>
</button>
</div>
)}
</div>
);
}

function MenuIcon() {
return (
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="8" y1="6" x2="21" y2="6" />
<line x1="8" y1="12" x2="21" y2="12" />
<line x1="8" y1="18" x2="21" y2="18" />
</svg>
);
}

function CopyIcon() {
return (
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
);
}

function CheckIcon() {
return (
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
);
}

function ChevronIcon({ isOpen }) {
return (
<svg
className={`${styles.icon} ${styles.chevron} ${isOpen ? styles.open : ''}`}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="6 9 12 15 18 9" />
</svg>
);
}

function ExternalLinkIcon() {
return (
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
<polyline points="15 3 21 3 21 9" />
<line x1="10" y1="14" x2="21" y2="3" />
</svg>
);
}

function LinkIcon() {
return (
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
);
}
123 changes: 123 additions & 0 deletions website/src/components/CopyMarkdownButton/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
.container {
position: relative;
display: inline-block;
}

.buttonGroup {
display: inline-flex;
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 6px;
overflow: hidden;
margin: 8px 0;
}

.copyButton {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
font-size: 13px;
font-weight: 500;
color: var(--ifm-color-emphasis-700);
background-color: var(--ifm-color-emphasis-100);
border: none;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 0;
}

.copyButton:hover:not(:disabled) {
background-color: var(--ifm-color-emphasis-200);
}

.copyButton:disabled {
cursor: not-allowed;
opacity: 0.7;
}

.dropdownButton {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 8px;
color: var(--ifm-color-emphasis-700);
background-color: var(--ifm-color-emphasis-100);
border: none;
border-left: 1px solid var(--ifm-color-emphasis-300);
cursor: pointer;
transition: all 0.2s ease;
border-radius: 0;
}

.dropdownButton:hover:not(:disabled) {
background-color: var(--ifm-color-emphasis-200);
}

.dropdownButton:disabled {
cursor: not-allowed;
opacity: 0.7;
}

.dropdown {
position: absolute;
right: 0;
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
min-width: 200px;
overflow: hidden;
}

.menuItem {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
font-size: 13px;
color: var(--ifm-color-emphasis-700);
border: none;
cursor: pointer;
text-align: left;
transition: all 0.2s ease;
background-color: var(--ifm-color-emphasis-100);
}

.menuItem:hover:not(:disabled) {
background-color: var(--ifm-color-emphasis-200);
}

.menuItem:disabled {
cursor: not-allowed;
opacity: 0.6;
}

.icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}

.chevron {
transition: transform 0.2s ease;
}

.chevron.open {
transform: rotate(180deg);
}

.spinner {
width: 14px;
height: 14px;
border: 2px solid var(--ifm-color-emphasis-300);
border-top-color: var(--ifm-color-emphasis-700);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}

@keyframes spin {
to {
transform: rotate(360deg);
}
}
Loading