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 ( +
+
+

{error}

+
+
+ ); + } + + if (!snippet) { + return ( +
+
+

Loading...

+
+
+ ); + } + + 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;