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 ? (
+
+
})
+
) : (
-
+
)}
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
;
+ },
}}
>
{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}`;
+};