diff --git a/client/src/App.tsx b/client/src/App.tsx
index dbb76d1..7bb1b18 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
+import { BrowserRouter as Router, Route, Routes, Navigate, useParams } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
import { ThemeProvider } from './contexts/ThemeContext';
import { useAuth } from './hooks/useAuth';
@@ -13,6 +13,7 @@ import SnippetStorage from './components/snippets/view/SnippetStorage';
import SharedSnippetView from './components/snippets/share/SharedSnippetView';
import SnippetPage from './components/snippets/view/SnippetPage';
import PublicSnippetStorage from './components/snippets/view/public/PublicSnippetStorage';
+import EmbedView from './components/snippets/embed/EmbedView';
const AuthenticatedApp: React.FC = () => {
const { isAuthenticated, isLoading } = useAuth();
@@ -34,6 +35,29 @@ const AuthenticatedApp: React.FC = () => {
return ;
};
+const EmbedViewWrapper: React.FC = () => {
+ const { shareId } = useParams();
+ const searchParams = new URLSearchParams(window.location.search);
+
+ if (!shareId) {
+ return
Invalid share ID
;
+ }
+
+ const theme = searchParams.get('theme') as 'light' | 'dark' | 'system' | null;
+
+ return (
+
+ );
+};
+
const App: React.FC = () => {
return (
@@ -47,6 +71,7 @@ const App: React.FC = () => {
} />
} />
} />
+ } />
} />
} />
diff --git a/client/src/components/common/buttons/CopyButton.tsx b/client/src/components/common/buttons/CopyButton.tsx
index 6f8a6fd..56979c3 100644
--- a/client/src/components/common/buttons/CopyButton.tsx
+++ b/client/src/components/common/buttons/CopyButton.tsx
@@ -53,4 +53,4 @@ const CopyButton: React.FC = ({ text }) => {
);
};
-export default CopyButton;
+export default CopyButton;
\ No newline at end of file
diff --git a/client/src/components/common/modals/Modal.tsx b/client/src/components/common/modals/Modal.tsx
index ab79dc0..56ff81e 100644
--- a/client/src/components/common/modals/Modal.tsx
+++ b/client/src/components/common/modals/Modal.tsx
@@ -27,7 +27,11 @@ const Modal: React.FC = ({
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
- if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
+ // Check if the click target is a modal backdrop (the semi-transparent overlay)
+ const isBackdropClick = (event.target as HTMLElement).classList.contains('modal-backdrop');
+
+ // Only close if clicking directly on the backdrop of this modal
+ if (isBackdropClick && modalRef.current?.parentElement === event.target) {
onClose();
}
};
@@ -56,7 +60,7 @@ const Modal: React.FC = ({
return (
= ({
);
};
-export default Modal;
\ No newline at end of file
+export default Modal;
diff --git a/client/src/components/editor/FullCodeBlock.tsx b/client/src/components/editor/FullCodeBlock.tsx
index d6cbe29..419de23 100644
--- a/client/src/components/editor/FullCodeBlock.tsx
+++ b/client/src/components/editor/FullCodeBlock.tsx
@@ -141,4 +141,4 @@ export const FullCodeBlock: React.FC = ({
);
-}
+}
\ No newline at end of file
diff --git a/client/src/components/snippets/embed/EmbedCodeView.tsx b/client/src/components/snippets/embed/EmbedCodeView.tsx
new file mode 100644
index 0000000..ea1e8ad
--- /dev/null
+++ b/client/src/components/snippets/embed/EmbedCodeView.tsx
@@ -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 = ({
+ 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("100px");
+ const containerRef = useRef(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 (
+
+
+
+ {isMarkdown ? (
+
+
+ {code}
+
+
+ ) : (
+
+
+ {code}
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/client/src/components/snippets/embed/EmbedCopyButton.tsx b/client/src/components/snippets/embed/EmbedCopyButton.tsx
new file mode 100644
index 0000000..3e14f6c
--- /dev/null
+++ b/client/src/components/snippets/embed/EmbedCopyButton.tsx
@@ -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 = ({ 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 (
+
+ );
+};
+
+export default EmbedCopyButton;
diff --git a/client/src/components/snippets/embed/EmbedModal.tsx b/client/src/components/snippets/embed/EmbedModal.tsx
new file mode 100644
index 0000000..85e5577
--- /dev/null
+++ b/client/src/components/snippets/embed/EmbedModal.tsx
@@ -0,0 +1,160 @@
+import React, { useState } from 'react';
+import { Code2 } from 'lucide-react';
+import Modal from '../../common/modals/Modal';
+import { Switch } from '../../common/switch/Switch';
+import { basePath } from '../../../utils/api/basePath';
+import { Snippet } from '../../../types/snippets';
+import { FullCodeBlock } from '../../editor/FullCodeBlock';
+import { generateEmbedId } from '../../../utils/helpers/embedUtils';
+
+interface EmbedModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ shareId: string;
+ snippet: Snippet;
+}
+
+export const EmbedModal: React.FC = ({
+ isOpen,
+ onClose,
+ shareId,
+ snippet
+}) => {
+ const [showTitle, setShowTitle] = useState(true);
+ const [showDescription, setShowDescription] = useState(true);
+ const [showFileHeaders, setShowFileHeaders] = useState(true);
+ const [showPoweredBy, setShowPoweredBy] = useState(true);
+ const [theme, setTheme] = useState<'light' | 'dark' | 'system'>('system');
+ const [selectedFragment, setSelectedFragment] = useState(undefined);
+
+ const getEmbedCode = () => {
+ const origin = window.location.origin;
+ const embedUrl = `${origin}${basePath}/embed/${shareId}?showTitle=${showTitle}&showDescription=${showDescription}&showFileHeaders=${showFileHeaders}&showPoweredBy=${showPoweredBy}&theme=${theme}${
+ selectedFragment !== undefined ? `&fragmentIndex=${selectedFragment}` : ''
+ }`;
+
+ const embedId = generateEmbedId({
+ shareId,
+ showTitle,
+ showDescription,
+ showFileHeaders,
+ showPoweredBy,
+ theme,
+ fragmentIndex: selectedFragment
+ });
+
+ return ``;
+ };
+
+ const handleModalClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ };
+
+ return (
+
+
+ Embed Snippet
+
+ }
+ >
+
+
+
Customize Embed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Embed Code
+
+
+
+
+ );
+};
+
+export default EmbedModal;
diff --git a/client/src/components/snippets/embed/EmbedView.tsx b/client/src/components/snippets/embed/EmbedView.tsx
new file mode 100644
index 0000000..9622363
--- /dev/null
+++ b/client/src/components/snippets/embed/EmbedView.tsx
@@ -0,0 +1,219 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { FileCode } from 'lucide-react';
+import { Snippet } from '../../../types/snippets';
+import { getLanguageLabel } from '../../../utils/language/languageUtils';
+import { basePath } from '../../../utils/api/basePath';
+import { generateEmbedId } from '../../../utils/helpers/embedUtils';
+import { EmbedCodeView } from './EmbedCodeView';
+
+interface EmbedViewProps {
+ shareId: string;
+ showTitle?: boolean;
+ showDescription?: boolean;
+ showFileHeaders?: boolean;
+ showPoweredBy?: boolean;
+ theme?: 'light' | 'dark' | 'blue' | 'system';
+ fragmentIndex?: number;
+}
+
+export const EmbedView: React.FC = ({
+ shareId,
+ showTitle = false,
+ showDescription = false,
+ showFileHeaders = true,
+ showPoweredBy = true,
+ theme = 'system',
+ fragmentIndex
+}) => {
+ const [snippet, setSnippet] = useState(null);
+ const [error, setError] = useState(null);
+ const containerRef = useRef(null);
+ const [effectiveTheme, setEffectiveTheme] = useState<'light' | 'dark' | 'blue'>(() => {
+ if (theme === 'system') {
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+ }
+ return theme;
+ });
+
+ const embedId = generateEmbedId({
+ shareId,
+ showTitle,
+ showDescription,
+ showFileHeaders,
+ showPoweredBy,
+ theme,
+ fragmentIndex
+ });
+
+ 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]);
+
+ useEffect(() => {
+ const fetchSnippet = async () => {
+ try {
+ const response = await fetch(
+ `${basePath}/api/embed/${shareId}?` +
+ new URLSearchParams({
+ showTitle: showTitle.toString(),
+ showDescription: showDescription.toString(),
+ showFileHeaders: showFileHeaders.toString(),
+ showPoweredBy: showPoweredBy.toString(),
+ theme: theme,
+ ...(fragmentIndex !== undefined && { fragmentIndex: fragmentIndex.toString() })
+ })
+ );
+
+ if (!response.ok) {
+ const data = await response.json();
+ throw new Error(data.error || 'Failed to load snippet');
+ }
+
+ const data = await response.json();
+ setSnippet(data);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to load snippet');
+ }
+ };
+
+ fetchSnippet();
+ }, [shareId, showTitle, showDescription, showFileHeaders, showPoweredBy, theme, fragmentIndex]);
+
+ useEffect(() => {
+ const updateHeight = () => {
+ if (containerRef.current) {
+ const height = containerRef.current.offsetHeight;
+ window.parent.postMessage({ type: 'resize', height, embedId }, '*');
+ }
+ };
+
+ updateHeight();
+
+ const observer = new ResizeObserver(updateHeight);
+ if (containerRef.current) {
+ observer.observe(containerRef.current);
+ }
+
+ return () => observer.disconnect();
+ }, [snippet, embedId]);
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (!snippet) {
+ return (
+
+ );
+ }
+
+ const getBackgroundColor = () => {
+ switch (effectiveTheme) {
+ case 'blue':
+ return 'bg-dark-surface';
+ case 'dark':
+ return 'bg-neutral-800';
+ case 'light':
+ return 'bg-light-surface';
+ }
+ };
+
+ const getHoverColor = () => {
+ switch (effectiveTheme) {
+ case 'blue':
+ return 'bg-dark-hover/50';
+ case 'dark':
+ return 'bg-neutral-700/50';
+ case 'light':
+ return 'bg-light-hover/50';
+ }
+ };
+
+ const getTextColor = () => {
+ if (effectiveTheme === 'light') {
+ return 'text-light-text';
+ }
+ return 'text-dark-text';
+ };
+
+ return (
+
+
+
+ {(showTitle || showDescription) && (
+
+ {showTitle && snippet.title && (
+
+ {snippet.title}
+
+ )}
+ {showDescription && snippet.description && (
+
+ {snippet.description}
+
+ )}
+
+ )}
+
+
+ {snippet.fragments.map((fragment, index) => (
+
+ {showFileHeaders && (
+
+
+
+ {fragment.file_name}
+
+
+ {getLanguageLabel(fragment.language)}
+
+
+ )}
+
+
+
+ ))}
+
+
+ {showPoweredBy && (
+
+
+ Powered by ByteStash
+
+
+ )}
+
+
+
+ );
+};
+
+export default EmbedView;
diff --git a/client/src/components/snippets/share/ShareMenu.tsx b/client/src/components/snippets/share/ShareMenu.tsx
index d1b6130..1c80d34 100644
--- a/client/src/components/snippets/share/ShareMenu.tsx
+++ b/client/src/components/snippets/share/ShareMenu.tsx
@@ -1,26 +1,29 @@
import React, { useState, useEffect } from 'react';
-import { Share as ShareIcon, Trash2, Link as LinkIcon, Check, ShieldCheck, ShieldOff } from 'lucide-react';
+import { Share as ShareIcon, Trash2, Link as LinkIcon, Check, ShieldCheck, ShieldOff, Code2 } from 'lucide-react';
import parseDuration from 'parse-duration';
import { formatDistanceToNow } from 'date-fns';
-import { Share, ShareSettings } from '../../../types/snippets';
+import { Share, ShareSettings, Snippet } from '../../../types/snippets';
import { useToast } from '../../../hooks/useToast';
import { createShare, deleteShare, getSharesBySnippetId } from '../../../utils/api/share';
import { basePath } from '../../../utils/api/basePath';
import Modal from '../../common/modals/Modal';
import { Switch } from '../../common/switch/Switch';
+import EmbedModal from '../embed/EmbedModal';
interface ShareMenuProps {
- snippetId: string;
isOpen: boolean;
onClose: () => void;
+ snippet: Snippet;
}
-export const ShareMenu: React.FC = ({ snippetId, isOpen, onClose }) => {
+export const ShareMenu: React.FC = ({ isOpen, onClose, snippet }) => {
const [shares, setShares] = useState([]);
const [requiresAuth, setRequiresAuth] = useState(false);
const [expiresIn, setExpiresIn] = useState('');
const [durationError, setDurationError] = useState('');
const [copiedStates, setCopiedStates] = useState>({});
+ const [selectedShareId, setSelectedShareId] = useState(null);
+ const [isEmbedModalOpen, setIsEmbedModalOpen] = useState(false);
const { addToast } = useToast();
useEffect(() => {
@@ -28,11 +31,11 @@ export const ShareMenu: React.FC = ({ snippetId, isOpen, onClose
loadShares();
setCopiedStates({});
}
- }, [isOpen, snippetId]);
+ }, [isOpen, snippet]);
const loadShares = async () => {
try {
- const loadedShares = await getSharesBySnippetId(snippetId);
+ const loadedShares = await getSharesBySnippetId(snippet.id);
setShares(loadedShares);
} catch (error) {
addToast('Failed to load shares', 'error');
@@ -55,7 +58,7 @@ export const ShareMenu: React.FC = ({ snippetId, isOpen, onClose
expiresIn: expiresIn ? Math.floor(parseDuration(expiresIn, 's')!) : undefined
};
- await createShare(snippetId, settings);
+ await createShare(snippet.id, settings);
await loadShares();
addToast('Share link created', 'success');
@@ -107,6 +110,11 @@ export const ShareMenu: React.FC = ({ snippetId, isOpen, onClose
}
};
+ const handleEmbedClick = (shareId: string) => {
+ setSelectedShareId(shareId);
+ setIsEmbedModalOpen(true);
+ };
+
const getRelativeExpiryTime = (expiresAt: string): string => {
try {
const expiryDate = new Date(expiresAt);
@@ -118,125 +126,156 @@ export const ShareMenu: React.FC = ({ snippetId, isOpen, onClose
};
return (
-
-
- Share Snippet
-
- }
- >
-
-
-
Create New Share Link
-
+ <>
+
+
+ Share Snippet
+
+ }
+ >
+
-
+
Create New Share Link
+
+
+
-
-
-
{
- setExpiresIn(e.target.value);
- setDurationError('');
- }}
- placeholder="Never"
- className="w-full px-3 py-2 bg-light-surface dark:bg-dark-surface text-light-text dark:text-dark-text rounded-md border border-light-border dark:border-dark-border focus:outline-none focus:ring-2 focus:ring-light-primary dark:focus:ring-dark-primary"
- />
- {durationError && (
-
{durationError}
- )}
-
+
+
+
{
+ setExpiresIn(e.target.value);
+ setDurationError('');
+ }}
+ placeholder="Never"
+ className="w-full px-3 py-2 bg-light-surface dark:bg-dark-surface text-light-text dark:text-dark-text rounded-md border border-light-border dark:border-dark-border focus:outline-none focus:ring-2 focus:ring-light-primary dark:focus:ring-dark-primary"
+ />
+ {durationError && (
+
{durationError}
+ )}
+
-
+
+
-
-
-
Active Share Links
-
- {shares.length === 0 ? (
-
No active share links
- ) : (
-
- {shares.map(share => (
-
-
-
-
{share.id}
- {share.requires_auth === 1 && (
-
-
-
- )}
- {share.requires_auth === 0 && (
-
-
-
- )}
-
- {share.expired === 1 && (
-
- Expired
-
- )}
- {share.expires_at && share.expired === 0 && (
-
- {getRelativeExpiryTime(share.expires_at)}
+
+
Active Share Links
+
+ {shares.length === 0 ? (
+
No active share links
+ ) : (
+
+ {shares.map(share => (
+
+
+
+
{share.id}
+ {share.requires_auth === 1 && (
+
+
)}
- {share.expires_at === null && (
-
- Never Expires
+ {share.requires_auth === 0 && (
+
+
)}
+
+ {share.expired === 1 && (
+
+ Expired
+
+ )}
+ {share.expires_at && share.expired === 0 && (
+
+ {getRelativeExpiryTime(share.expires_at)}
+
+ )}
+ {share.expires_at === null && (
+
+ Never Expires
+
+ )}
+
+
+
+
+
+
-
-
-
-
-
- ))}
-
- )}
+ ))}
+
+ )}
+
-
-
+
+
+ {selectedShareId && (
+
{
+ setIsEmbedModalOpen(false);
+ setSelectedShareId(null);
+ }}
+ shareId={selectedShareId}
+ snippet={snippet}
+ />
+ )}
+ >
);
-};
+}
diff --git a/client/src/components/snippets/view/SnippetStorage.tsx b/client/src/components/snippets/view/SnippetStorage.tsx
index 14420b7..3b29989 100644
--- a/client/src/components/snippets/view/SnippetStorage.tsx
+++ b/client/src/components/snippets/view/SnippetStorage.tsx
@@ -149,7 +149,7 @@ const SnippetStorage: React.FC = () => {
{snippetToShare && (
diff --git a/client/src/constants/routes.ts b/client/src/constants/routes.ts
index 83ed65b..353aa19 100644
--- a/client/src/constants/routes.ts
+++ b/client/src/constants/routes.ts
@@ -5,5 +5,6 @@ export const ROUTES = {
LOGIN: '/login',
REGISTER: '/register',
PUBLIC_SNIPPETS: '/public/snippets',
- AUTH_CALLBACK: '/auth/callback'
-} as const;
\ No newline at end of file
+ AUTH_CALLBACK: '/auth/callback',
+ EMBED: '/embed/:shareId'
+} as const;
diff --git a/client/src/utils/helpers/embedUtils.ts b/client/src/utils/helpers/embedUtils.ts
new file mode 100644
index 0000000..5c224fc
--- /dev/null
+++ b/client/src/utils/helpers/embedUtils.ts
@@ -0,0 +1,23 @@
+interface EmbedParams {
+ shareId: string;
+ showTitle: boolean;
+ showDescription: boolean;
+ showFileHeaders: boolean;
+ showPoweredBy: boolean;
+ theme: string;
+ fragmentIndex?: number;
+}
+
+export const generateEmbedId = (params: EmbedParams): string => {
+ const paramsString = `${params.shareId}-${params.showTitle}-${params.showDescription}-${params.showFileHeaders}-${params.showPoweredBy}-${params.fragmentIndex ?? 'all'}`;
+
+ let hash = 0;
+ for (let i = 0; i < paramsString.length; i++) {
+ const char = paramsString.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash;
+ }
+
+ const hashStr = Math.abs(hash).toString(16).padStart(16, '0').slice(0, 16);
+ return hashStr;
+};
diff --git a/server/src/app.js b/server/src/app.js
index 31f5dfe..79343b5 100644
--- a/server/src/app.js
+++ b/server/src/app.js
@@ -5,6 +5,7 @@ import authRoutes from './routes/authRoutes.js';
import shareRoutes from './routes/shareRoutes.js';
import publicRoutes from './routes/publicRoutes.js';
import oidcRoutes from './routes/oidcRoutes.js';
+import embedRoutes from './routes/embedRoutes.js';
import { authenticateToken } from './middleware/auth.js';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
@@ -29,6 +30,7 @@ app.use(`${basePath}/api/auth/oidc`, oidcRoutes);
app.use(`${basePath}/api/snippets`, authenticateToken, snippetRoutes);
app.use(`${basePath}/api/share`, shareRoutes);
app.use(`${basePath}/api/public/snippets`, publicRoutes);
+app.use(`${basePath}/api/embed`, embedRoutes);
app.use(`${basePath}/assets`, express.static(assetsPath));
app.use(`${basePath}/monacoeditorwork`, express.static(join(buildPath, 'monacoeditorwork')));
@@ -100,4 +102,4 @@ function handleShutdown() {
})();
process.on('SIGTERM', handleShutdown);
-process.on('SIGINT', handleShutdown);
\ No newline at end of file
+process.on('SIGINT', handleShutdown);
diff --git a/server/src/routes/embedRoutes.js b/server/src/routes/embedRoutes.js
new file mode 100644
index 0000000..0375dcd
--- /dev/null
+++ b/server/src/routes/embedRoutes.js
@@ -0,0 +1,43 @@
+import express from 'express';
+import shareRepository from '../repositories/shareRepository.js';
+
+const router = express.Router();
+
+router.get('/:shareId', async (req, res) => {
+ try {
+ const { shareId } = req.params;
+ const { showTitle, showDescription, fragmentIndex } = req.query;
+
+ const snippet = await shareRepository.getShare(shareId);
+ if (!snippet) {
+ return res.status(404).json({ error: 'Snippet not found' });
+ }
+
+ if (snippet.share.expired) {
+ return res.status(404).json({ error: 'Share link has expired' });
+ }
+
+ if (snippet.share.requiresAuth && !req.user) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const embedData = {
+ id: snippet.id,
+ title: showTitle === 'true' ? snippet.title : undefined,
+ description: showDescription === 'true' ? snippet.description : undefined,
+ language: snippet.language,
+ fragments: fragmentIndex !== undefined ?
+ [snippet.fragments[parseInt(fragmentIndex, 10)]] :
+ snippet.fragments,
+ created_at: snippet.created_at,
+ updated_at: snippet.updated_at
+ };
+
+ res.json(embedData);
+ } catch (error) {
+ console.error('Error in embed route:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+export default router;