From 43d647c9ea0d4e0e05495febb0028a79c55cc156 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sun, 29 Sep 2024 23:44:24 +0200 Subject: [PATCH] Add support for images --- backend/internal/api/handlers.go | 20 +++++++- frontend/src/App.scss | 15 ++++++ frontend/src/components/FileTree.js | 12 ++++- frontend/src/components/MainContent.js | 56 +++++++++++++++++++--- frontend/src/components/MarkdownPreview.js | 10 +++- frontend/src/hooks/useFileManagement.js | 13 ++++- frontend/src/services/api.js | 4 ++ 7 files changed, 118 insertions(+), 12 deletions(-) diff --git a/backend/internal/api/handlers.go b/backend/internal/api/handlers.go index 16dc7a4..7161480 100644 --- a/backend/internal/api/handlers.go +++ b/backend/internal/api/handlers.go @@ -4,6 +4,7 @@ import ( "encoding/json" "io" "net/http" + "path/filepath" "strconv" "strings" @@ -36,7 +37,24 @@ func GetFileContent(fs *filesystem.FileSystem) http.HandlerFunc { return } - w.Header().Set("Content-Type", "text/plain") + // Determine content type based on file extension + contentType := "text/plain" + switch filepath.Ext(filePath) { + case ".png": + contentType = "image/png" + case ".jpg", ".jpeg": + contentType = "image/jpeg" + case ".webp": + contentType = "image/webp" + case ".gif": + contentType = "image/gif" + case ".svg": + contentType = "image/svg+xml" + case ".md": + contentType = "text/markdown" + } + + w.Header().Set("Content-Type", contentType) if _, err := w.Write(content); err != nil { http.Error(w, "Failed to write response", http.StatusInternalServerError) } diff --git a/frontend/src/App.scss b/frontend/src/App.scss index 2ccfb53..df3fcec 100644 --- a/frontend/src/App.scss +++ b/frontend/src/App.scss @@ -22,6 +22,21 @@ $navbar-height: 64px; } } +.image-preview { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + overflow: auto; +} + +.image-preview img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + .page-content { padding: 0 $padding; } diff --git a/frontend/src/components/FileTree.js b/frontend/src/components/FileTree.js index b440757..276f783 100644 --- a/frontend/src/components/FileTree.js +++ b/frontend/src/components/FileTree.js @@ -7,6 +7,7 @@ import { GitCommit, Plus, Trash, + Image, } from '@geist-ui/icons'; const FileTree = ({ @@ -37,8 +38,15 @@ const FileTree = ({ ); }; - const renderIcon = ({ type }) => - type === 'directory' ? : ; + const isImageFile = (fileName) => { + const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']; + return imageExtensions.some((ext) => fileName.toLowerCase().endsWith(ext)); + }; + + const renderIcon = ({ type, name }) => { + if (type === 'directory') return ; + return isImageFile(name) ? : ; + }; return (
diff --git a/frontend/src/components/MainContent.js b/frontend/src/components/MainContent.js index 8e6fdee..4ef8f21 100644 --- a/frontend/src/components/MainContent.js +++ b/frontend/src/components/MainContent.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Grid, Breadcrumbs, @@ -14,7 +14,17 @@ import { Code, Eye } from '@geist-ui/icons'; import Editor from './Editor'; import FileTree from './FileTree'; import MarkdownPreview from './MarkdownPreview'; -import { commitAndPush, saveFileContent, deleteFile } from '../services/api'; +import { + commitAndPush, + saveFileContent, + deleteFile, + getFileUrl, +} from '../services/api'; + +const isImageFile = (filePath) => { + const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']; + return imageExtensions.some((ext) => filePath.toLowerCase().endsWith(ext)); +}; const MainContent = ({ content, @@ -33,6 +43,21 @@ const MainContent = ({ const { setToast } = useToasts(); const [newFileModalVisible, setNewFileModalVisible] = useState(false); const [newFileName, setNewFileName] = useState(''); + const [isCurrentFileImage, setIsCurrentFileImage] = useState(false); + + useEffect(() => { + const currentFileIsImage = isImageFile(selectedFile); + setIsCurrentFileImage(currentFileIsImage); + if (currentFileIsImage) { + setActiveTab('preview'); + } + }, [selectedFile]); + + const handleTabChange = (value) => { + if (!isCurrentFileImage || value === 'preview') { + setActiveTab(value); + } + }; const handlePull = async () => { try { @@ -153,13 +178,17 @@ const MainContent = ({ >
{renderBreadcrumbs()} - - } value="source" /> + + } + value="source" + disabled={isCurrentFileImage} + /> } value="preview" />
- {activeTab === 'source' ? ( + {activeTab === 'source' && !isCurrentFileImage ? ( + ) : isCurrentFileImage ? ( +
+ {selectedFile} +
) : ( - + )}
diff --git a/frontend/src/components/MarkdownPreview.js b/frontend/src/components/MarkdownPreview.js index 8f7af2d..45fe5cc 100644 --- a/frontend/src/components/MarkdownPreview.js +++ b/frontend/src/components/MarkdownPreview.js @@ -6,7 +6,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; import 'katex/dist/katex.min.css'; -const MarkdownPreview = ({ content }) => { +const MarkdownPreview = ({ content, baseUrl }) => { return (
{ ); }, + img({ src, alt, ...props }) { + // Check if the src is a relative path + if (src && !src.startsWith('http') && !src.startsWith('data:')) { + // Prepend the baseUrl to create an absolute path + src = `${baseUrl}/files/${src}`; + } + return {alt}; + }, }} > {content} diff --git a/frontend/src/hooks/useFileManagement.js b/frontend/src/hooks/useFileManagement.js index 2105816..9d837d5 100644 --- a/frontend/src/hooks/useFileManagement.js +++ b/frontend/src/hooks/useFileManagement.js @@ -13,6 +13,11 @@ const DEFAULT_FILE = { content: '# Welcome to NovaMD\n\nStart editing here!', }; +const isImageFile = (filePath) => { + const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']; + return imageExtensions.some((ext) => filePath.toLowerCase().endsWith(ext)); +}; + const useFileManagement = (gitEnabled = false) => { const [content, setContent] = useState(DEFAULT_FILE.content); const [files, setFiles] = useState([]); @@ -76,8 +81,12 @@ const useFileManagement = (gitEnabled = false) => { } try { - const fileContent = await fetchFileContent(filePath); - setContent(fileContent); + if (!isImageFile(filePath)) { + const fileContent = await fetchFileContent(filePath); + setContent(fileContent); + } else { + setContent(''); // Set empty content for image files + } setSelectedFile(filePath); setIsNewFile(false); setHasUnsavedChanges(false); diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 151cc1c..e780740 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -127,3 +127,7 @@ export const commitAndPush = async (message) => { throw error; } }; + +export const getFileUrl = (filePath) => { + return `${API_BASE_URL}/files/${filePath}`; +};