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
11 changes: 9 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@
.docusaurus
.cache-loader
.history
llms.txt
llms-full.txt
static/llms.txt
static/llms-full.txt
static/intro-kots.md
static/intro-replicated.md
static/intro.md
static/enterprise/*
static/reference/*
static/release-notes/*
static/vendor/*

# Misc
.DS_Store
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"dev": "docusaurus clear && docusaurus start",
"prebuild": "npm run generate-llms",
"build": "docusaurus build",
"rebuild-serve": "npm run build && npm run serve",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
Expand Down
249 changes: 249 additions & 0 deletions src/components/CopyMarkdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import styles from './CopyMarkdown.module.css';

function CopyMarkdown() {
const [isOpen, setIsOpen] = useState(false);
const [hasContent, setHasContent] = useState(false);
const [isDarkTheme, setIsDarkTheme] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const buttonRef = useRef(null);
const dropdownRef = useRef(null);

// Toggle dropdown
const toggleDropdown = useCallback(() => {
setIsOpen(prev => !prev);
}, []);

// Copy markdown to clipboard
const copyMarkdown = useCallback(async () => {
try {
// Get the current page path
const currentPath = window.location.pathname;

// Remove trailing slash if it exists
const normalizedPath = currentPath.endsWith('/') && currentPath !== '/'
? currentPath.slice(0, -1)
: currentPath;

// For the homepage/intro, use /intro.md specifically
const markdownPath = normalizedPath === '/' ? '/intro.md' : `${normalizedPath}.md`;

// Fetch the markdown content
const response = await fetch(markdownPath);

if (!response.ok) {
throw new Error(`Failed to fetch markdown: ${response.status}`);
}

const markdown = await response.text();

// Copy to clipboard
await navigator.clipboard.writeText(markdown);

// Close dropdown and show copied feedback
setIsOpen(false);
setIsCopied(true);

// Reset the button state after 2 seconds
setTimeout(() => {
setIsCopied(false);
}, 2000);
} catch (error) {
console.error('Failed to copy markdown:', error);
}
}, []);

// View as plain text
const viewAsMarkdown = useCallback(() => {
try {
// Get the current page path
const currentPath = window.location.pathname;

// Remove trailing slash if it exists
const normalizedPath = currentPath.endsWith('/') && currentPath !== '/'
? currentPath.slice(0, -1)
: currentPath;

// For the homepage/intro, use /intro.md specifically
const markdownPath = normalizedPath === '/' ? '/intro.md' : `${normalizedPath}.md`;

// Open in a new tab
window.open(markdownPath, '_blank');

// Close dropdown
setIsOpen(false);
} catch (error) {
console.error('Failed to view markdown:', error);
}
}, []);

// Open in ChatGPT
const openInChatGpt = useCallback(async () => {
try {
// Get the current page path
const currentPath = window.location.pathname;

// Remove trailing slash if it exists
const normalizedPath = currentPath.endsWith('/') && currentPath !== '/'
? currentPath.slice(0, -1)
: currentPath;

// For the homepage/intro, use /intro specifically
const docPath = normalizedPath === '/' ? '/intro' : normalizedPath;

// Construct the full markdown URL with domain
const fullMarkdownUrl = `https://docs.replicated.com${docPath}.md`;

// Create the prompt to send to ChatGPT
const prompt = `Read ${fullMarkdownUrl} so I can ask questions about it`;

// URL encode the prompt for the ChatGPT URL
const encodedPrompt = encodeURIComponent(prompt);

// Create the ChatGPT URL with the prompt
const chatGptUrl = `https://chat.openai.com/?prompt=${encodedPrompt}`;

// Open ChatGPT with the prompt
window.open(chatGptUrl, '_blank');

// Close the dropdown
setIsOpen(false);
} catch (error) {
console.error('Failed to open ChatGPT:', error);
}
}, []);

// Handle click outside to close dropdown
const handleClickOutside = useCallback((event) => {
// Only close if clicking outside both the button and dropdown
if (
buttonRef.current &&
!buttonRef.current.contains(event.target) &&
dropdownRef.current &&
!dropdownRef.current.contains(event.target)
) {
setIsOpen(false);
}
}, []);

// Initialize on client side
useEffect(() => {
// Check if we have markdown content
const hasMarkdownContent = !!document.querySelector('.theme-doc-markdown.markdown h1');
setHasContent(hasMarkdownContent);

// Set up click outside handler
document.addEventListener('click', handleClickOutside);

return () => {
// Clean up event listener
document.removeEventListener('click', handleClickOutside);
};
}, [handleClickOutside]);

// Check for dark theme
useEffect(() => {
const checkTheme = () => {
setIsDarkTheme(document.documentElement.getAttribute('data-theme') === 'dark');
};

checkTheme();

// Listen for theme changes
const observer = new MutationObserver(checkTheme);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme']
});

return () => observer.disconnect();
}, []);

// Don't render on pages that don't have markdown content
if (!hasContent) {
return null;
}

return (
<div className={styles.container}>
<button
ref={buttonRef}
className={`${styles.button} ${isCopied ? styles.copied : ''}`}
onClick={!isCopied ? toggleDropdown : undefined}
aria-expanded={isOpen}
aria-haspopup="true"
disabled={isCopied}
>
{isCopied ? (
<span className={styles.buttonText}>Copied!</span>
) : (
<>
<img
src={isDarkTheme ? "/images/icons/copy-white.svg" : "/images/icons/copy.svg"}
alt="Copy"
className={styles.copyIcon}
/>
<span className={styles.buttonText}>Open in ChatGPT</span>
<svg className={styles.icon} viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</>
)}
</button>

{isOpen && (
<div
ref={dropdownRef}
className={styles.dropdown}
role="menu"
aria-orientation="vertical"
>
<ul className={styles.list}>
<li className={styles.item}>
<button
className={styles.actionButton}
onClick={openInChatGpt}
role="menuitem"
>
<div className={styles.actionContent}>
<span className={styles.actionTitle}>Open in ChatGPT</span>
<span className={styles.actionDescription}>Ask questions about this page</span>
</div>
</button>
</li>
<li className={styles.item}>
<button
className={styles.actionButton}
onClick={copyMarkdown}
role="menuitem"
>
<div className={styles.actionContent}>
<span className={styles.actionTitle}>Copy Markdown</span>
<span className={styles.actionDescription}>Copy page source to clipboard</span>
</div>
</button>
</li>
<li className={styles.item}>
<button
className={styles.actionButton}
onClick={viewAsMarkdown}
role="menuitem"
>
<div className={styles.actionContent}>
<span className={styles.actionTitle}>View Markdown</span>
<span className={styles.actionDescription}>Open this page in plain text format</span>
</div>
</button>
</li>
</ul>
</div>
)}
</div>
);
}

export default CopyMarkdown;
Loading