Skip to content

Commit

Permalink
Created new embed components to keep app and embed separate, added da…
Browse files Browse the repository at this point in the history
…rk theme: light, dark, blue, system.
  • Loading branch information
jordan-dalby committed Nov 30, 2024
1 parent e1ef5f0 commit f03e0ba
Show file tree
Hide file tree
Showing 5 changed files with 339 additions and 81 deletions.
33 changes: 10 additions & 23 deletions client/src/components/common/buttons/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import React, { useState } from 'react';
import { Copy, Check } from 'lucide-react';
import { useTheme } from '../../../contexts/ThemeContext';

export interface CopyButtonProps {
text: string;
forceTheme?: 'light' | 'dark' | null;
}

const CopyButton: React.FC<CopyButtonProps> = ({ text, forceTheme = null }) => {
const CopyButton: React.FC<CopyButtonProps> = ({ text }) => {
const [isCopied, setIsCopied] = useState(false);
const { theme } = useTheme();
const isDark = forceTheme ? forceTheme == 'dark' : theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)

const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation();
try {
const isEmbedded = window !== window.parent;

if (isEmbedded || !navigator.clipboard || !window.isSecureContext) {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
} else {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
Expand All @@ -28,42 +24,33 @@ const CopyButton: React.FC<CopyButtonProps> = ({ text, forceTheme = null }) => {
textArea.select();

try {
const successful = document.execCommand('copy');
if (!successful) {
throw new Error('Copy command failed');
}
document.execCommand('copy');
} finally {
textArea.remove();
}
} else {
await navigator.clipboard.writeText(text);
}

setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
} catch (err) {
console.error('Failed to copy text: ', err);
setIsCopied(false);
}
};

return (
<button
onClick={handleCopy}
className={`absolute top-2 right-2 p-1 rounded-md transition-colors ${
isDark
? `bg-dark-surface hover:bg-dark-hover text-dark-text`
: `bg-light-surface hover:bg-light-hover text-light-text`
}`}
className="absolute top-2 right-2 p-1 bg-light-surface dark:bg-dark-surface rounded-md
hover:bg-light-hover dark:hover:bg-dark-hover transition-colors text-light-text dark:text-dark-text"
title="Copy to clipboard"
>
{isCopied ? (
<Check size={16} className={isDark ? "text-dark-primary" : "text-light-primary"} />
<Check size={16} className="text-light-primary dark:text-dark-primary" />
) : (
<Copy size={16} className={isDark ? "text-dark-text" : "text-light-text"} />
<Copy size={16} className="text-light-text dark:text-dark-text" />
)}
</button>
);
};

export default CopyButton;
export default CopyButton;
24 changes: 3 additions & 21 deletions client/src/components/editor/FullCodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ export interface FullCodeBlockProps {
code: string;
language?: string;
showLineNumbers?: boolean;
forceTheme?: 'light' | 'dark' | null;
}

export const FullCodeBlock: React.FC<FullCodeBlockProps> = ({
code,
language = 'plaintext',
showLineNumbers = true,
forceTheme = null
showLineNumbers = true
}) => {
const { theme } = useTheme();
const [effectiveTheme, setEffectiveTheme] = useState<'light' | 'dark'>(
Expand All @@ -27,11 +25,6 @@ export const FullCodeBlock: React.FC<FullCodeBlockProps> = ({
);

useEffect(() => {
if (forceTheme) {
setEffectiveTheme(forceTheme);
return;
}

if (theme === 'system') {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
Expand Down Expand Up @@ -99,17 +92,6 @@ export const FullCodeBlock: React.FC<FullCodeBlockProps> = ({
border-radius: 0.5rem;
position: relative;
}
.markdown-content-full pre,
.markdown-content-full code {
background-color: ${isDark ? '#2d2d2d' : '#ebebeb'} !important;
color: ${isDark ? '#e5e7eb' : '#1f2937'} !important;
}
.markdown-content-full pre code {
background-color: transparent !important;
padding: 0;
border: none;
box-shadow: none;
}
:root {
--text-color: ${isDark ? '#ffffff' : '#000000'};
}
Expand Down Expand Up @@ -155,8 +137,8 @@ export const FullCodeBlock: React.FC<FullCodeBlockProps> = ({
</div>
)}

<CopyButton text={code} forceTheme={forceTheme}/>
<CopyButton text={code} />
</div>
</div>
);
}
}
167 changes: 167 additions & 0 deletions client/src/components/snippets/embed/EmbedCodeView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React, { useEffect, useRef, useState } from 'react';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus, oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import { getLanguageLabel, getMonacoLanguage } from '../../../utils/language/languageUtils';
import EmbedCopyButton from './EmbedCopyButton';

export interface EmbedCodeBlockProps {
code: string;
language?: string;
showLineNumbers?: boolean;
theme?: 'light' | 'dark' | 'blue' | 'system';
}

export const EmbedCodeView: React.FC<EmbedCodeBlockProps> = ({
code,
language = 'plaintext',
showLineNumbers = true,
theme = 'system'
}) => {
const [effectiveTheme, setEffectiveTheme] = useState<'light' | 'dark' | 'blue'>(() => {
if (theme === 'system') {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return theme;
});

useEffect(() => {
if (theme !== 'system') {
setEffectiveTheme(theme);
return;
}

const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
setEffectiveTheme(mediaQuery.matches ? 'dark' : 'light');
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [theme]);

const isDark = effectiveTheme === 'dark' || effectiveTheme === 'blue';
const isMarkdown = getLanguageLabel(language) === 'markdown';
const [highlighterHeight, setHighlighterHeight] = useState<string>("100px");
const containerRef = useRef<HTMLDivElement>(null);
const LINE_HEIGHT = 19;

useEffect(() => {
updateHighlighterHeight();
const resizeObserver = new ResizeObserver(updateHighlighterHeight);
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
return () => resizeObserver.disconnect();
}, [code]);

const updateHighlighterHeight = () => {
if (!containerRef.current) return;

const lineCount = code.split('\n').length;
const contentHeight = (lineCount * LINE_HEIGHT) + 35;
const newHeight = Math.min(500, Math.max(100, contentHeight));
setHighlighterHeight(`${newHeight}px`);
};

const baseTheme = isDark ? vscDarkPlus : oneLight;
const getBackgroundColor = () => {
switch (effectiveTheme) {
case 'blue':
case 'dark':
return '#1E1E1E';
case 'light':
return '#ffffff';
}
};

const backgroundColor = getBackgroundColor();
const customStyle = {
...baseTheme,
'pre[class*="language-"]': {
...baseTheme['pre[class*="language-"]'],
margin: 0,
fontSize: '13px',
background: backgroundColor,
padding: '1rem',
},
'code[class*="language-"]': {
...baseTheme['code[class*="language-"]'],
fontSize: '13px',
background: backgroundColor,
display: 'block',
textIndent: 0,
}
};

return (
<div className="relative">
<style>
{`
.markdown-content-full {
color: var(--text-color);
background-color: ${backgroundColor};
padding: 1rem;
border-radius: 0.5rem;
position: relative;
}
.markdown-content-full pre,
.markdown-content-full code {
background-color: ${isDark ? '#2d2d2d' : '#ebebeb'} !important;
color: ${isDark ? '#e5e7eb' : '#1f2937'} !important;
}
.markdown-content-full pre code {
background-color: transparent !important;
padding: 0;
border: none;
box-shadow: none;
}
:root {
--text-color: ${isDark ? '#ffffff' : '#000000'};
}
`}
</style>
<div className="relative">
{isMarkdown ? (
<div className="markdown-content markdown-content-full rounded-lg" style={{ backgroundColor }}>
<ReactMarkdown className={`markdown prose ${isDark ? 'prose-invert' : ''} max-w-none`}>
{code}
</ReactMarkdown>
</div>
) : (
<div
ref={containerRef}
style={{ maxHeight: '500px' }}
>
<SyntaxHighlighter
language={getMonacoLanguage(language)}
style={customStyle}
showLineNumbers={showLineNumbers}
wrapLines={true}
lineProps={{
style: {
whiteSpace: 'pre',
wordBreak: 'break-all',
paddingLeft: 0
}
}}
customStyle={{
height: highlighterHeight,
minHeight: '100px',
marginBottom: 0,
marginTop: 0,
textIndent: 0,
paddingLeft: showLineNumbers ? 10 : 20,
borderRadius: '0.5rem',
background: backgroundColor
}}
>
{code}
</SyntaxHighlighter>
</div>
)}

<EmbedCopyButton text={code} theme={theme} />
</div>
</div>
);
};
91 changes: 91 additions & 0 deletions client/src/components/snippets/embed/EmbedCopyButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { useState } from 'react';
import { Copy, Check } from 'lucide-react';

export interface EmbedCopyButtonProps {
text: string;
theme: 'light' | 'dark' | 'blue' | 'system';
}

const EmbedCopyButton: React.FC<EmbedCopyButtonProps> = ({ text, theme }) => {
const [isCopied, setIsCopied] = useState(false);

const isDark = theme === 'dark' || theme === 'blue' ||
(theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);

const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation();
try {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();

try {
const successful = document.execCommand('copy');
if (!successful) {
throw new Error('Copy command failed');
}
} finally {
textArea.remove();
}

setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
} catch (err) {
console.error('Failed to copy text: ', err);
setIsCopied(false);
}
};

const getBackgroundColor = () => {
switch (theme) {
case 'blue':
return 'bg-dark-surface hover:bg-dark-hover';
case 'dark':
return 'bg-neutral-700 hover:bg-neutral-600';
case 'light':
return 'bg-light-surface hover:bg-light-hover';
case 'system':
return isDark
? 'bg-neutral-700 hover:bg-neutral-600'
: 'bg-light-surface hover:bg-light-hover';
}
};

const getTextColor = () => {
if (theme === 'blue' || theme === 'dark' || (theme === 'system' && isDark)) {
return 'text-dark-text';
}
return 'text-light-text';
};

const getIconColor = () => {
if (isCopied) {
return isDark ? 'text-dark-primary' : 'text-light-primary';
}
if (theme === 'blue' || theme === 'dark' || (theme === 'system' && isDark)) {
return 'text-dark-text';
}
return 'text-light-text';
};

return (
<button
onClick={handleCopy}
className={`absolute top-2 right-2 p-1 rounded-md transition-colors ${getBackgroundColor()} ${getTextColor()}`}
title="Copy to clipboard"
>
{isCopied ? (
<Check size={16} className={getIconColor()} />
) : (
<Copy size={16} className={getIconColor()} />
)}
</button>
);
};

export default EmbedCopyButton;
Loading

0 comments on commit f03e0ba

Please sign in to comment.