mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-06 07:54:22 +00:00
@@ -7,6 +7,7 @@ import (
|
||||
"novamd/internal/models"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -17,9 +18,10 @@ type FileSystem struct {
|
||||
}
|
||||
|
||||
type FileNode struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Files []FileNode `json:"files,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Children []FileNode `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
func New(rootDir string, settings *models.Settings) *FileSystem {
|
||||
@@ -73,40 +75,52 @@ func (fs *FileSystem) validatePath(path string) (string, error) {
|
||||
}
|
||||
|
||||
func (fs *FileSystem) ListFilesRecursively() ([]FileNode, error) {
|
||||
return fs.walkDirectory(fs.RootDir)
|
||||
return fs.walkDirectory(fs.RootDir, "")
|
||||
}
|
||||
|
||||
func (fs *FileSystem) walkDirectory(dir string) ([]FileNode, error) {
|
||||
var nodes []FileNode
|
||||
|
||||
func (fs *FileSystem) walkDirectory(dir, prefix string) ([]FileNode, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var folders []FileNode
|
||||
var files []FileNode
|
||||
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
path := filepath.Join(prefix, name)
|
||||
fullPath := filepath.Join(dir, name)
|
||||
|
||||
if entry.IsDir() {
|
||||
subdir := filepath.Join(dir, entry.Name())
|
||||
subFiles, err := fs.walkDirectory(subdir)
|
||||
children, err := fs.walkDirectory(fullPath, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodes = append(nodes, FileNode{
|
||||
Type: "directory",
|
||||
Name: entry.Name(),
|
||||
Files: subFiles,
|
||||
folders = append(folders, FileNode{
|
||||
ID: path, // Using path as ID ensures uniqueness
|
||||
Name: name,
|
||||
Path: path,
|
||||
Children: children,
|
||||
})
|
||||
} else {
|
||||
nodes = append(nodes, FileNode{
|
||||
Type: "file",
|
||||
Name: entry.Name(),
|
||||
files = append(files, FileNode{
|
||||
ID: path, // Using path as ID ensures uniqueness
|
||||
Name: name,
|
||||
Path: path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nodes, nil
|
||||
// Sort folders and files alphabetically
|
||||
sort.Slice(folders, func(i, j int) bool { return folders[i].Name < folders[j].Name })
|
||||
sort.Slice(files, func(i, j int) bool { return files[i].Name < files[i].Name })
|
||||
|
||||
// Combine folders and files, with folders first
|
||||
return append(folders, files...), nil
|
||||
}
|
||||
|
||||
|
||||
func (fs *FileSystem) FindFileByName(filenameOrPath string) ([]string, error) {
|
||||
var foundPaths []string
|
||||
var searchPattern string
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"]
|
||||
}
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
["@babel/preset-react", { "runtime": "automatic" }]
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-transform-class-properties",
|
||||
"@babel/plugin-transform-runtime"
|
||||
]
|
||||
}
|
||||
|
||||
2222
frontend/package-lock.json
generated
2222
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -27,11 +27,16 @@
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@codemirror/view": "^6.34.0",
|
||||
"@geist-ui/core": "^2.3.8",
|
||||
"@geist-ui/icons": "^1.0.2",
|
||||
"@mantine/core": "^7.13.2",
|
||||
"@mantine/hooks": "^7.13.2",
|
||||
"@mantine/modals": "^7.13.2",
|
||||
"@mantine/notifications": "^7.13.2",
|
||||
"@react-hook/resize-observer": "^2.0.2",
|
||||
"@tabler/icons-react": "^3.19.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"katex": "^0.16.11",
|
||||
"react": "^18.3.1",
|
||||
"react-arborist": "^3.4.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
@@ -57,13 +62,20 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@babel/core": "^7.25.7",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-transform-class-properties": "^7.25.7",
|
||||
"@babel/plugin-transform-runtime": "^7.25.7",
|
||||
"@babel/preset-env": "^7.25.7",
|
||||
"@babel/preset-react": "^7.25.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"sass": "^1.79.3",
|
||||
"postcss": "^8.4.47",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"sass": "^1.79.4",
|
||||
"sass-loader": "^16.0.2",
|
||||
"style-loader": "^4.0.0",
|
||||
"webpack": "^5.94.0",
|
||||
|
||||
14
frontend/postcss.config.js
Normal file
14
frontend/postcss.config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
'mantine-breakpoint-xs': '36em',
|
||||
'mantine-breakpoint-sm': '48em',
|
||||
'mantine-breakpoint-md': '62em',
|
||||
'mantine-breakpoint-lg': '75em',
|
||||
'mantine-breakpoint-xl': '88em',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,38 +1,39 @@
|
||||
import React from 'react';
|
||||
import { GeistProvider, CssBaseline, Page } from '@geist-ui/core';
|
||||
import Header from './components/Header';
|
||||
import MainContent from './components/MainContent';
|
||||
import { MantineProvider, ColorSchemeScript } from '@mantine/core';
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
import { ModalsProvider } from '@mantine/modals';
|
||||
import Layout from './components/Layout';
|
||||
import { SettingsProvider, useSettings } from './contexts/SettingsContext';
|
||||
import { ModalProvider } from './contexts/ModalContext';
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import './App.scss';
|
||||
|
||||
function AppContent() {
|
||||
const { settings, loading } = useSettings();
|
||||
const { loading } = useSettings();
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<GeistProvider themeType={settings.theme}>
|
||||
<CssBaseline />
|
||||
<Page>
|
||||
<Header />
|
||||
<Page.Content className="page-content">
|
||||
<MainContent />
|
||||
</Page.Content>
|
||||
</Page>
|
||||
</GeistProvider>
|
||||
);
|
||||
return <Layout />;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<SettingsProvider>
|
||||
<ModalProvider>
|
||||
<AppContent />
|
||||
</ModalProvider>
|
||||
</SettingsProvider>
|
||||
<>
|
||||
<ColorSchemeScript defaultColorScheme="light" />
|
||||
<MantineProvider defaultColorScheme="light">
|
||||
<Notifications />
|
||||
<ModalsProvider>
|
||||
<SettingsProvider>
|
||||
<ModalProvider>
|
||||
<AppContent />
|
||||
</ModalProvider>
|
||||
</SettingsProvider>
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -108,26 +108,3 @@ $navbar-height: 64px;
|
||||
.tree {
|
||||
padding-top: $padding;
|
||||
}
|
||||
|
||||
// Geist UI Tree component customization
|
||||
:global {
|
||||
.file-tree {
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.selected {
|
||||
color: #0070f3;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Text, Center } from '@mantine/core';
|
||||
import Editor from './Editor';
|
||||
import MarkdownPreview from './MarkdownPreview';
|
||||
import { Text } from '@geist-ui/core';
|
||||
import { getFileUrl } from '../services/api';
|
||||
import { isImageFile } from '../utils/fileHelpers';
|
||||
|
||||
@@ -15,22 +15,17 @@ const ContentView = ({
|
||||
}) => {
|
||||
if (!selectedFile) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<Text h3>No file selected.</Text>
|
||||
</div>
|
||||
<Center style={{ height: '100%' }}>
|
||||
<Text size="xl" weight={500}>
|
||||
No file selected.
|
||||
</Text>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
if (isImageFile(selectedFile)) {
|
||||
return (
|
||||
<div className="image-preview">
|
||||
<Center className="image-preview">
|
||||
<img
|
||||
src={getFileUrl(selectedFile)}
|
||||
alt={selectedFile}
|
||||
@@ -40,7 +35,7 @@ const ContentView = ({
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Button, Tooltip, ButtonGroup, Spacer } from '@geist-ui/core';
|
||||
import { Plus, Trash, GitPullRequest, GitCommit } from '@geist-ui/icons';
|
||||
import { ActionIcon, Tooltip, Group } from '@mantine/core';
|
||||
import {
|
||||
IconPlus,
|
||||
IconTrash,
|
||||
IconGitPullRequest,
|
||||
IconGitCommit,
|
||||
} from '@tabler/icons-react';
|
||||
import { useSettings } from '../contexts/SettingsContext';
|
||||
import { useModalContext } from '../contexts/ModalContext';
|
||||
|
||||
@@ -17,70 +22,63 @@ const FileActions = ({ handlePullChanges, selectedFile }) => {
|
||||
const handleCommitAndPush = () => setCommitMessageModalVisible(true);
|
||||
|
||||
return (
|
||||
<ButtonGroup className="file-actions">
|
||||
<Tooltip text="Create new file" type="dark">
|
||||
<Button
|
||||
icon={<Plus />}
|
||||
auto
|
||||
scale={2 / 3}
|
||||
onClick={handleCreateFile}
|
||||
px={0.6}
|
||||
/>
|
||||
<Group gap="xs">
|
||||
<Tooltip label="Create new file">
|
||||
<ActionIcon variant="default" size="md" onClick={handleCreateFile}>
|
||||
<IconPlus size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Spacer w={0.5} />
|
||||
|
||||
<Tooltip
|
||||
text={selectedFile ? 'Delete current file' : 'No file selected'}
|
||||
type="dark"
|
||||
label={selectedFile ? 'Delete current file' : 'No file selected'}
|
||||
>
|
||||
<Button
|
||||
icon={<Trash />}
|
||||
auto
|
||||
scale={2 / 3}
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="md"
|
||||
onClick={handleDeleteFile}
|
||||
disabled={!selectedFile}
|
||||
type="error"
|
||||
px={0.6}
|
||||
/>
|
||||
color="red"
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Spacer w={0.5} />
|
||||
|
||||
<Tooltip
|
||||
text={
|
||||
label={
|
||||
settings.gitEnabled
|
||||
? 'Pull changes from remote'
|
||||
: 'Git is not enabled'
|
||||
}
|
||||
type="dark"
|
||||
>
|
||||
<Button
|
||||
icon={<GitPullRequest />}
|
||||
auto
|
||||
scale={2 / 3}
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="md"
|
||||
onClick={handlePullChanges}
|
||||
disabled={!settings.gitEnabled}
|
||||
px={0.6}
|
||||
/>
|
||||
>
|
||||
<IconGitPullRequest size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Spacer w={0.5} />
|
||||
|
||||
<Tooltip
|
||||
text={
|
||||
label={
|
||||
!settings.gitEnabled
|
||||
? 'Git is not enabled'
|
||||
: settings.gitAutoCommit
|
||||
? 'Auto-commit is enabled'
|
||||
: 'Commit and push changes'
|
||||
}
|
||||
type="dark"
|
||||
>
|
||||
<Button
|
||||
icon={<GitCommit />}
|
||||
auto
|
||||
scale={2 / 3}
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="md"
|
||||
onClick={handleCommitAndPush}
|
||||
disabled={!settings.gitEnabled || settings.gitAutoCommit}
|
||||
px={0.6}
|
||||
/>
|
||||
>
|
||||
<IconGitCommit size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,28 +1,102 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Tree } from '@geist-ui/core';
|
||||
import { File, Folder, Image } from '@geist-ui/icons';
|
||||
import { isImageFile } from '../utils/fileHelpers';
|
||||
import React, { useRef, useState, useLayoutEffect } from 'react';
|
||||
import { Tree } from 'react-arborist';
|
||||
import { IconFile, IconFolder, IconFolderOpen } from '@tabler/icons-react';
|
||||
import { Tooltip } from '@mantine/core';
|
||||
import useResizeObserver from '@react-hook/resize-observer';
|
||||
|
||||
const useSize = (target) => {
|
||||
const [size, setSize] = useState();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setSize(target.current.getBoundingClientRect());
|
||||
}, [target]);
|
||||
|
||||
useResizeObserver(target, (entry) => setSize(entry.contentRect));
|
||||
return size;
|
||||
};
|
||||
|
||||
const FileIcon = ({ node }) => {
|
||||
if (node.isLeaf) {
|
||||
return <IconFile size={16} />;
|
||||
}
|
||||
return node.isOpen ? (
|
||||
<IconFolderOpen size={16} color="var(--mantine-color-yellow-filled)" />
|
||||
) : (
|
||||
<IconFolder size={16} color="var(--mantine-color-yellow-filled)" />
|
||||
);
|
||||
};
|
||||
|
||||
const Node = ({ node, style, dragHandle }) => {
|
||||
return (
|
||||
<Tooltip label={node.data.name} openDelay={500}>
|
||||
<div
|
||||
ref={dragHandle}
|
||||
style={{
|
||||
...style,
|
||||
paddingLeft: `${node.level * 20}px`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (node.isInternal) {
|
||||
node.toggle();
|
||||
} else {
|
||||
node.tree.props.onNodeClick(node);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FileIcon node={node} />
|
||||
<span
|
||||
style={{
|
||||
marginLeft: '8px',
|
||||
fontSize: '14px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
{node.data.name}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const FileTree = ({ files, handleFileSelect }) => {
|
||||
if (files.length === 0) {
|
||||
return <div>No files to display</div>;
|
||||
}
|
||||
|
||||
const renderIcon = useMemo(
|
||||
() =>
|
||||
({ type, name }) => {
|
||||
if (type === 'directory') return <Folder />;
|
||||
return isImageFile(name) ? <Image /> : <File />;
|
||||
},
|
||||
[]
|
||||
);
|
||||
const target = useRef(null);
|
||||
const size = useSize(target);
|
||||
|
||||
return (
|
||||
<Tree
|
||||
value={files}
|
||||
onClick={(filePath) => handleFileSelect(filePath)}
|
||||
renderIcon={renderIcon}
|
||||
/>
|
||||
<div
|
||||
ref={target}
|
||||
style={{ height: 'calc(100vh - 140px)', marginTop: '20px' }}
|
||||
>
|
||||
{size && (
|
||||
<Tree
|
||||
data={files}
|
||||
openByDefault={false}
|
||||
width={size.width}
|
||||
height={size.height}
|
||||
indent={24}
|
||||
rowHeight={28}
|
||||
onActivate={(node) => {
|
||||
if (!node.isInternal) {
|
||||
handleFileSelect(node.data.path);
|
||||
}
|
||||
}}
|
||||
onNodeClick={(node) => {
|
||||
if (!node.isInternal) {
|
||||
handleFileSelect(node.data.path);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{Node}
|
||||
</Tree>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Page, Text, User, Button, Spacer } from '@geist-ui/core';
|
||||
import { Settings as SettingsIcon } from '@geist-ui/icons';
|
||||
import { Group, Text, ActionIcon, Avatar } from '@mantine/core';
|
||||
import { IconSettings } from '@tabler/icons-react';
|
||||
import Settings from './Settings';
|
||||
import { useModalContext } from '../contexts/ModalContext';
|
||||
|
||||
@@ -10,14 +10,18 @@ const Header = () => {
|
||||
const openSettings = () => setSettingsModalVisible(true);
|
||||
|
||||
return (
|
||||
<Page.Header className="custom-navbar">
|
||||
<Text b>NovaMD</Text>
|
||||
<Spacer w={1} />
|
||||
<User src="https://via.placeholder.com/40" name="User" />
|
||||
<Spacer w={0.5} />
|
||||
<Button auto icon={<SettingsIcon />} onClick={openSettings} />
|
||||
<Group justify="space-between" h={60} px="md">
|
||||
<Text fw={700} size="lg">
|
||||
NovaMD
|
||||
</Text>
|
||||
<Group>
|
||||
<Avatar src="https://via.placeholder.com/40" radius="xl" />
|
||||
<ActionIcon variant="subtle" onClick={openSettings} size="lg">
|
||||
<IconSettings size={24} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
<Settings />
|
||||
</Page.Header>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
42
frontend/src/components/Layout.js
Normal file
42
frontend/src/components/Layout.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { AppShell, Container } from '@mantine/core';
|
||||
import Header from './Header';
|
||||
import Sidebar from './Sidebar';
|
||||
import MainContent from './MainContent';
|
||||
import { useFileNavigation } from '../hooks/useFileNavigation';
|
||||
|
||||
const Layout = () => {
|
||||
const { selectedFile, handleFileSelect, handleLinkClick } =
|
||||
useFileNavigation();
|
||||
|
||||
return (
|
||||
<AppShell header={{ height: 60 }} padding="md">
|
||||
<AppShell.Header>
|
||||
<Header />
|
||||
</AppShell.Header>
|
||||
<AppShell.Main>
|
||||
<Container
|
||||
size="xl"
|
||||
p={0}
|
||||
style={{
|
||||
display: 'flex',
|
||||
height: 'calc(100vh - 60px - 2rem)', // Subtracting header height and vertical padding
|
||||
overflow: 'hidden', // Prevent scrolling in the container
|
||||
}}
|
||||
>
|
||||
<Sidebar
|
||||
selectedFile={selectedFile}
|
||||
handleFileSelect={handleFileSelect}
|
||||
/>
|
||||
<MainContent
|
||||
selectedFile={selectedFile}
|
||||
handleFileSelect={handleFileSelect}
|
||||
handleLinkClick={handleLinkClick}
|
||||
/>
|
||||
</Container>
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -1,27 +1,20 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { Breadcrumbs, Grid, Tabs, Dot } from '@geist-ui/core';
|
||||
import { Code, Eye } from '@geist-ui/icons';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { Tabs, Breadcrumbs, Group, Box, Text, Flex } from '@mantine/core';
|
||||
import { IconCode, IconEye, IconPointFilled } from '@tabler/icons-react';
|
||||
|
||||
import FileActions from './FileActions';
|
||||
import FileTree from './FileTree';
|
||||
import ContentView from './ContentView';
|
||||
import CreateFileModal from './modals/CreateFileModal';
|
||||
import DeleteFileModal from './modals/DeleteFileModal';
|
||||
import CommitMessageModal from './modals/CommitMessageModal';
|
||||
|
||||
import { useFileContent } from '../hooks/useFileContent';
|
||||
import { useFileList } from '../hooks/useFileList';
|
||||
import { useFileOperations } from '../hooks/useFileOperations';
|
||||
import { useGitOperations } from '../hooks/useGitOperations';
|
||||
import { useFileNavigation } from '../hooks/useFileNavigation';
|
||||
import { useSettings } from '../contexts/SettingsContext';
|
||||
|
||||
const MainContent = () => {
|
||||
const MainContent = ({ selectedFile, handleFileSelect, handleLinkClick }) => {
|
||||
const [activeTab, setActiveTab] = useState('source');
|
||||
const { settings } = useSettings();
|
||||
const { files, loadFileList } = useFileList();
|
||||
const { handleLinkClick, selectedFile, handleFileSelect } =
|
||||
useFileNavigation();
|
||||
const {
|
||||
content,
|
||||
hasUnsavedChanges,
|
||||
@@ -29,15 +22,11 @@ const MainContent = () => {
|
||||
handleContentChange,
|
||||
} = useFileContent(selectedFile);
|
||||
const { handleSave, handleCreate, handleDelete } = useFileOperations();
|
||||
const { handleCommitAndPush, handlePull } = useGitOperations();
|
||||
const { handleCommitAndPush } = useGitOperations(settings.gitEnabled);
|
||||
|
||||
useEffect(() => {
|
||||
loadFileList();
|
||||
}, [settings.gitEnabled]);
|
||||
|
||||
const handleTabChange = (value) => {
|
||||
const handleTabChange = useCallback((value) => {
|
||||
setActiveTab(value);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSaveFile = useCallback(
|
||||
async (filePath, content) => {
|
||||
@@ -54,91 +43,79 @@ const MainContent = () => {
|
||||
async (fileName) => {
|
||||
const success = await handleCreate(fileName);
|
||||
if (success) {
|
||||
await loadFileList();
|
||||
handleFileSelect(fileName);
|
||||
}
|
||||
},
|
||||
[handleCreate, loadFileList, handleFileSelect]
|
||||
[handleCreate, handleFileSelect]
|
||||
);
|
||||
|
||||
const handleDeleteFile = useCallback(
|
||||
async (filePath) => {
|
||||
const success = await handleDelete(filePath);
|
||||
if (success) {
|
||||
await loadFileList();
|
||||
handleFileSelect(null);
|
||||
}
|
||||
},
|
||||
[handleDelete, loadFileList, handleFileSelect]
|
||||
[handleDelete, handleFileSelect]
|
||||
);
|
||||
|
||||
const renderBreadcrumbs = () => {
|
||||
if (!selectedFile) return <div className="breadcrumbs-container"></div>;
|
||||
const renderBreadcrumbs = useMemo(() => {
|
||||
if (!selectedFile) return null;
|
||||
const pathParts = selectedFile.split('/');
|
||||
const items = pathParts.map((part, index) => (
|
||||
<Text key={index} size="sm">
|
||||
{part}
|
||||
</Text>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="breadcrumbs-container">
|
||||
<Breadcrumbs>
|
||||
{pathParts.map((part, index) => (
|
||||
<Breadcrumbs.Item key={index}>{part}</Breadcrumbs.Item>
|
||||
))}
|
||||
</Breadcrumbs>
|
||||
<Group>
|
||||
<Breadcrumbs separator="/">{items}</Breadcrumbs>
|
||||
{hasUnsavedChanges && (
|
||||
<Dot type="warning" className="unsaved-indicator" />
|
||||
<IconPointFilled
|
||||
size={16}
|
||||
style={{ color: 'var(--mantine-color-yellow-filled)' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
}, [selectedFile, hasUnsavedChanges]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid.Container gap={1} height="calc(100vh - 64px)">
|
||||
<Grid xs={24} sm={6} md={5} lg={4} height="100%" className="sidebar">
|
||||
<div className="file-tree-container">
|
||||
<FileActions
|
||||
handlePullChanges={handlePull}
|
||||
selectedFile={selectedFile}
|
||||
/>
|
||||
<FileTree
|
||||
files={files}
|
||||
selectedFile={selectedFile}
|
||||
handleFileSelect={handleFileSelect}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid
|
||||
xs={24}
|
||||
sm={18}
|
||||
md={19}
|
||||
lg={20}
|
||||
height="100%"
|
||||
className="main-content"
|
||||
>
|
||||
<div className="content-header">
|
||||
{renderBreadcrumbs()}
|
||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||
<Tabs.Item label={<Code />} value="source" />
|
||||
<Tabs.Item label={<Eye />} value="preview" />
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className="content-body">
|
||||
<ContentView
|
||||
activeTab={activeTab}
|
||||
selectedFile={selectedFile}
|
||||
content={content}
|
||||
handleContentChange={handleContentChange}
|
||||
handleSave={handleSaveFile}
|
||||
handleLinkClick={handleLinkClick}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid.Container>
|
||||
<Box
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Flex justify="space-between" align="center" p="md">
|
||||
{renderBreadcrumbs}
|
||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="source" leftSection={<IconCode size="0.8rem" />} />
|
||||
<Tabs.Tab value="preview" leftSection={<IconEye size="0.8rem" />} />
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
</Flex>
|
||||
<Box style={{ flex: 1, overflow: 'auto' }}>
|
||||
<ContentView
|
||||
activeTab={activeTab}
|
||||
selectedFile={selectedFile}
|
||||
content={content}
|
||||
handleContentChange={handleContentChange}
|
||||
handleSave={handleSaveFile}
|
||||
handleLinkClick={handleLinkClick}
|
||||
/>
|
||||
</Box>
|
||||
<CreateFileModal onCreateFile={handleCreateFile} />
|
||||
<DeleteFileModal
|
||||
onDeleteFile={handleDeleteFile}
|
||||
selectedFile={selectedFile}
|
||||
/>
|
||||
<CommitMessageModal onCommitAndPush={handleCommitAndPush} />
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useReducer, useEffect, useCallback, useRef } from 'react';
|
||||
import { Modal, Spacer, Dot, useToasts } from '@geist-ui/core';
|
||||
import { Modal, Badge, Button, Group, Title } from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { useSettings } from '../contexts/SettingsContext';
|
||||
import AppearanceSettings from './settings/AppearanceSettings';
|
||||
import EditorSettings from './settings/EditorSettings';
|
||||
@@ -49,12 +50,10 @@ function settingsReducer(state, action) {
|
||||
}
|
||||
|
||||
const Settings = () => {
|
||||
const { settings, updateSettings, updateTheme } = useSettings();
|
||||
const { settings, updateSettings, colorScheme } = useSettings();
|
||||
const { settingsModalVisible, setSettingsModalVisible } = useModalContext();
|
||||
const { setToast } = useToasts();
|
||||
const [state, dispatch] = useReducer(settingsReducer, initialState);
|
||||
const isInitialMount = useRef(true);
|
||||
const updateThemeTimeoutRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialMount.current) {
|
||||
@@ -63,92 +62,78 @@ const Settings = () => {
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({
|
||||
type: 'UPDATE_LOCAL_SETTINGS',
|
||||
payload: { theme: colorScheme },
|
||||
});
|
||||
}, [colorScheme]);
|
||||
|
||||
const handleInputChange = useCallback((key, value) => {
|
||||
dispatch({ type: 'UPDATE_LOCAL_SETTINGS', payload: { [key]: value } });
|
||||
}, []);
|
||||
|
||||
const handleThemeChange = useCallback(() => {
|
||||
const newTheme = state.localSettings.theme === 'dark' ? 'light' : 'dark';
|
||||
dispatch({ type: 'UPDATE_LOCAL_SETTINGS', payload: { theme: newTheme } });
|
||||
|
||||
// Debounce the theme update
|
||||
if (updateThemeTimeoutRef.current) {
|
||||
clearTimeout(updateThemeTimeoutRef.current);
|
||||
}
|
||||
updateThemeTimeoutRef.current = setTimeout(() => {
|
||||
updateTheme(newTheme);
|
||||
}, 0);
|
||||
}, [state.localSettings.theme, updateTheme]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await updateSettings(state.localSettings);
|
||||
dispatch({ type: 'MARK_SAVED' });
|
||||
setToast({ text: 'Settings saved successfully', type: 'success' });
|
||||
notifications.show({
|
||||
message: 'Settings saved successfully',
|
||||
color: 'green',
|
||||
});
|
||||
setSettingsModalVisible(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error);
|
||||
setToast({
|
||||
text: 'Failed to save settings: ' + error.message,
|
||||
type: 'error',
|
||||
notifications.show({
|
||||
message: 'Failed to save settings: ' + error.message,
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (state.hasUnsavedChanges) {
|
||||
updateTheme(state.initialSettings.theme); // Revert theme if not saved
|
||||
dispatch({ type: 'RESET' });
|
||||
}
|
||||
setSettingsModalVisible(false);
|
||||
}, [
|
||||
state.hasUnsavedChanges,
|
||||
state.initialSettings.theme,
|
||||
updateTheme,
|
||||
setSettingsModalVisible,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (updateThemeTimeoutRef.current) {
|
||||
clearTimeout(updateThemeTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [state.hasUnsavedChanges, setSettingsModalVisible]);
|
||||
|
||||
return (
|
||||
<Modal visible={settingsModalVisible} onClose={handleClose}>
|
||||
<Modal.Title>
|
||||
Settings
|
||||
{state.hasUnsavedChanges && (
|
||||
<Dot type="warning" style={{ marginLeft: '8px' }} />
|
||||
)}
|
||||
</Modal.Title>
|
||||
<Modal.Content>
|
||||
<AppearanceSettings
|
||||
themeSettings={state.localSettings.theme}
|
||||
onThemeChange={handleThemeChange}
|
||||
/>
|
||||
<Spacer h={1} />
|
||||
<EditorSettings
|
||||
autoSave={state.localSettings.autoSave}
|
||||
onAutoSaveChange={(value) => handleInputChange('autoSave', value)}
|
||||
/>
|
||||
<Spacer h={1} />
|
||||
<GitSettings
|
||||
gitEnabled={state.localSettings.gitEnabled}
|
||||
gitUrl={state.localSettings.gitUrl}
|
||||
gitUser={state.localSettings.gitUser}
|
||||
gitToken={state.localSettings.gitToken}
|
||||
gitAutoCommit={state.localSettings.gitAutoCommit}
|
||||
gitCommitMsgTemplate={state.localSettings.gitCommitMsgTemplate}
|
||||
onInputChange={handleInputChange}
|
||||
/>
|
||||
</Modal.Content>
|
||||
<Modal.Action passive onClick={handleClose}>
|
||||
Cancel
|
||||
</Modal.Action>
|
||||
<Modal.Action onClick={handleSubmit}>Save Changes</Modal.Action>
|
||||
<Modal
|
||||
opened={settingsModalVisible}
|
||||
onClose={handleClose}
|
||||
title={<Title order={2}>Settings</Title>}
|
||||
centered
|
||||
size="lg"
|
||||
>
|
||||
{state.hasUnsavedChanges && (
|
||||
<Badge color="yellow" variant="light" mb="md">
|
||||
Unsaved Changes
|
||||
</Badge>
|
||||
)}
|
||||
<AppearanceSettings
|
||||
themeSettings={state.localSettings.theme}
|
||||
onThemeChange={(newTheme) => handleInputChange('theme', newTheme)}
|
||||
/>
|
||||
<EditorSettings
|
||||
autoSave={state.localSettings.autoSave}
|
||||
onAutoSaveChange={(value) => handleInputChange('autoSave', value)}
|
||||
/>
|
||||
<GitSettings
|
||||
gitEnabled={state.localSettings.gitEnabled}
|
||||
gitUrl={state.localSettings.gitUrl}
|
||||
gitUser={state.localSettings.gitUser}
|
||||
gitToken={state.localSettings.gitToken}
|
||||
gitAutoCommit={state.localSettings.gitAutoCommit}
|
||||
gitCommitMsgTemplate={state.localSettings.gitCommitMsgTemplate}
|
||||
onInputChange={handleInputChange}
|
||||
/>
|
||||
<Group justify="flex-end" mt="xl">
|
||||
<Button variant="default" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>Save Changes</Button>
|
||||
</Group>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
39
frontend/src/components/Sidebar.js
Normal file
39
frontend/src/components/Sidebar.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Box } from '@mantine/core';
|
||||
import FileActions from './FileActions';
|
||||
import FileTree from './FileTree';
|
||||
import { useFileList } from '../hooks/useFileList';
|
||||
import { useGitOperations } from '../hooks/useGitOperations';
|
||||
import { useSettings } from '../contexts/SettingsContext';
|
||||
|
||||
const Sidebar = ({ selectedFile, handleFileSelect }) => {
|
||||
const { settings } = useSettings();
|
||||
const { files, loadFileList } = useFileList();
|
||||
const { handlePull } = useGitOperations(settings.gitEnabled);
|
||||
|
||||
useEffect(() => {
|
||||
loadFileList();
|
||||
}, [settings.gitEnabled, loadFileList]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
width: '25%',
|
||||
minWidth: '200px',
|
||||
maxWidth: '300px',
|
||||
borderRight: '1px solid var(--app-shell-border-color)',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<FileActions handlePullChanges={handlePull} selectedFile={selectedFile} />
|
||||
<FileTree
|
||||
files={files}
|
||||
handleFileSelect={handleFileSelect}
|
||||
selectedFile={selectedFile}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, Input } from '@geist-ui/core';
|
||||
import { Modal, TextInput, Button, Group, Box } from '@mantine/core';
|
||||
import { useModalContext } from '../../contexts/ModalContext';
|
||||
|
||||
const CommitMessageModal = ({ onCommitAndPush }) => {
|
||||
@@ -17,22 +17,31 @@ const CommitMessageModal = ({ onCommitAndPush }) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={commitMessageModalVisible}
|
||||
opened={commitMessageModalVisible}
|
||||
onClose={() => setCommitMessageModalVisible(false)}
|
||||
title="Enter Commit Message"
|
||||
centered
|
||||
size="sm"
|
||||
>
|
||||
<Modal.Title>Enter Commit Message</Modal.Title>
|
||||
<Modal.Content>
|
||||
<Input
|
||||
width="100%"
|
||||
<Box maw={400} mx="auto">
|
||||
<TextInput
|
||||
label="Commit Message"
|
||||
placeholder="Enter commit message"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onChange={(event) => setMessage(event.currentTarget.value)}
|
||||
mb="md"
|
||||
w="100%"
|
||||
/>
|
||||
</Modal.Content>
|
||||
<Modal.Action passive onClick={() => setCommitMessageModalVisible(false)}>
|
||||
Cancel
|
||||
</Modal.Action>
|
||||
<Modal.Action onClick={handleSubmit}>Commit</Modal.Action>
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => setCommitMessageModalVisible(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>Commit</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, Input } from '@geist-ui/core';
|
||||
import { Modal, TextInput, Button, Group, Box } from '@mantine/core';
|
||||
import { useModalContext } from '../../contexts/ModalContext';
|
||||
|
||||
const CreateFileModal = ({ onCreateFile }) => {
|
||||
@@ -16,22 +16,31 @@ const CreateFileModal = ({ onCreateFile }) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={newFileModalVisible}
|
||||
opened={newFileModalVisible}
|
||||
onClose={() => setNewFileModalVisible(false)}
|
||||
title="Create New File"
|
||||
centered
|
||||
size="sm"
|
||||
>
|
||||
<Modal.Title>Create New File</Modal.Title>
|
||||
<Modal.Content>
|
||||
<Input
|
||||
width="100%"
|
||||
<Box maw={400} mx="auto">
|
||||
<TextInput
|
||||
label="File Name"
|
||||
placeholder="Enter file name"
|
||||
value={fileName}
|
||||
onChange={(e) => setFileName(e.target.value)}
|
||||
onChange={(event) => setFileName(event.currentTarget.value)}
|
||||
mb="md"
|
||||
w="100%"
|
||||
/>
|
||||
</Modal.Content>
|
||||
<Modal.Action passive onClick={() => setNewFileModalVisible(false)}>
|
||||
Cancel
|
||||
</Modal.Action>
|
||||
<Modal.Action onClick={handleSubmit}>Create</Modal.Action>
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => setNewFileModalVisible(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>Create</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Modal, Text } from '@geist-ui/core';
|
||||
import { Modal, Text, Button, Group } from '@mantine/core';
|
||||
import { useModalContext } from '../../contexts/ModalContext';
|
||||
|
||||
const DeleteFileModal = ({ onDeleteFile, selectedFile }) => {
|
||||
@@ -13,17 +13,23 @@ const DeleteFileModal = ({ onDeleteFile, selectedFile }) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={deleteFileModalVisible}
|
||||
opened={deleteFileModalVisible}
|
||||
onClose={() => setDeleteFileModalVisible(false)}
|
||||
title="Delete File"
|
||||
centered
|
||||
>
|
||||
<Modal.Title>Delete File</Modal.Title>
|
||||
<Modal.Content>
|
||||
<Text>Are you sure you want to delete "{selectedFile}"?</Text>
|
||||
</Modal.Content>
|
||||
<Modal.Action passive onClick={() => setDeleteFileModalVisible(false)}>
|
||||
Cancel
|
||||
</Modal.Action>
|
||||
<Modal.Action onClick={handleConfirm}>Delete</Modal.Action>
|
||||
<Text>Are you sure you want to delete "{selectedFile}"?</Text>
|
||||
<Group justify="flex-end" mt="xl">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => setDeleteFileModalVisible(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="red" onClick={handleConfirm}>
|
||||
Delete
|
||||
</Button>
|
||||
</Group>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import React from 'react';
|
||||
import { Text, Toggle } from '@geist-ui/core';
|
||||
import { Text, Switch, Group, Box, Title } from '@mantine/core';
|
||||
import { useSettings } from '../../contexts/SettingsContext';
|
||||
|
||||
const AppearanceSettings = ({ onThemeChange }) => {
|
||||
const { colorScheme, toggleColorScheme } = useSettings();
|
||||
|
||||
const handleThemeChange = () => {
|
||||
toggleColorScheme();
|
||||
onThemeChange(colorScheme === 'dark' ? 'light' : 'dark');
|
||||
};
|
||||
|
||||
const AppearanceSettings = ({ themeSettings, onThemeChange }) => {
|
||||
return (
|
||||
<div className="setting-group">
|
||||
<Text h4>Appearance</Text>
|
||||
<div className="setting-item">
|
||||
<Text>Dark Mode</Text>
|
||||
<Toggle checked={themeSettings === 'dark'} onChange={onThemeChange} />
|
||||
</div>
|
||||
</div>
|
||||
<Box mb="md">
|
||||
<Title order={3} mb="md">
|
||||
Appearance
|
||||
</Title>
|
||||
<Group justify="space-between" align="center">
|
||||
<Text size="sm">Dark Mode</Text>
|
||||
<Switch checked={colorScheme === 'dark'} onChange={handleThemeChange} />
|
||||
</Group>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Text, Toggle, Tooltip } from '@geist-ui/core';
|
||||
import { Text, Switch, Tooltip, Group, Box, Title } from '@mantine/core';
|
||||
|
||||
const EditorSettings = ({ autoSave, onAutoSaveChange }) => {
|
||||
return (
|
||||
<div className="setting-group">
|
||||
<Text h4>Editor</Text>
|
||||
<div className="setting-item">
|
||||
<Text>Auto Save</Text>
|
||||
<Tooltip
|
||||
text="Auto Save feature is coming soon!"
|
||||
type="dark"
|
||||
placement="left"
|
||||
>
|
||||
<Toggle
|
||||
<Box mb="md">
|
||||
<Title order={3} mb="md">
|
||||
Editor
|
||||
</Title>
|
||||
<Tooltip label="Auto Save feature is coming soon!" position="left">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text size="sm">Auto Save</Text>
|
||||
<Switch
|
||||
checked={autoSave}
|
||||
onChange={(e) => onAutoSaveChange(e.target.checked)}
|
||||
onChange={(event) => onAutoSaveChange(event.currentTarget.checked)}
|
||||
disabled
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Group>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import React from 'react';
|
||||
import { Text, Toggle, Input, Spacer } from '@geist-ui/core';
|
||||
import {
|
||||
Text,
|
||||
Switch,
|
||||
TextInput,
|
||||
Stack,
|
||||
PasswordInput,
|
||||
Group,
|
||||
Title,
|
||||
Grid,
|
||||
} from '@mantine/core';
|
||||
|
||||
const GitSettings = ({
|
||||
gitEnabled,
|
||||
@@ -11,60 +20,95 @@ const GitSettings = ({
|
||||
onInputChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className="setting-group">
|
||||
<Text h4>Git Integration</Text>
|
||||
<div className="setting-item">
|
||||
<Text>Enable Git</Text>
|
||||
<Toggle
|
||||
checked={gitEnabled}
|
||||
onChange={(e) => onInputChange('gitEnabled', e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className={gitEnabled ? '' : 'disabled'}>
|
||||
<Input
|
||||
width="100%"
|
||||
label="Git URL"
|
||||
value={gitUrl}
|
||||
onChange={(e) => onInputChange('gitUrl', e.target.value)}
|
||||
disabled={!gitEnabled}
|
||||
/>
|
||||
<Spacer h={0.5} />
|
||||
<Input
|
||||
width="100%"
|
||||
label="Git Username"
|
||||
value={gitUser}
|
||||
onChange={(e) => onInputChange('gitUser', e.target.value)}
|
||||
disabled={!gitEnabled}
|
||||
/>
|
||||
<Spacer h={0.5} />
|
||||
<Input.Password
|
||||
width="100%"
|
||||
label="Git Token"
|
||||
value={gitToken}
|
||||
onChange={(e) => onInputChange('gitToken', e.target.value)}
|
||||
disabled={!gitEnabled}
|
||||
/>
|
||||
<Spacer h={0.5} />
|
||||
<div className="setting-item">
|
||||
<Text>Auto Commit</Text>
|
||||
<Toggle
|
||||
checked={gitAutoCommit}
|
||||
onChange={(e) => onInputChange('gitAutoCommit', e.target.checked)}
|
||||
<Stack spacing="md">
|
||||
<Title order={3}>Git Integration</Title>
|
||||
<Grid gutter="md" align="center">
|
||||
<Grid.Col span={6}>
|
||||
<Text size="sm">Enable Git</Text>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<Group justify="flex-end">
|
||||
<Switch
|
||||
checked={gitEnabled}
|
||||
onChange={(event) =>
|
||||
onInputChange('gitEnabled', event.currentTarget.checked)
|
||||
}
|
||||
/>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<Text size="sm">Git URL</Text>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
value={gitUrl}
|
||||
onChange={(event) =>
|
||||
onInputChange('gitUrl', event.currentTarget.value)
|
||||
}
|
||||
disabled={!gitEnabled}
|
||||
placeholder="Enter Git URL"
|
||||
/>
|
||||
</div>
|
||||
<Spacer h={0.5} />
|
||||
<Input
|
||||
width="100%"
|
||||
label="Commit Message Template"
|
||||
value={gitCommitMsgTemplate}
|
||||
onChange={(e) =>
|
||||
onInputChange('gitCommitMsgTemplate', e.target.value)
|
||||
}
|
||||
disabled={!gitEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<Text size="sm">Git Username</Text>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
value={gitUser}
|
||||
onChange={(event) =>
|
||||
onInputChange('gitUser', event.currentTarget.value)
|
||||
}
|
||||
disabled={!gitEnabled}
|
||||
placeholder="Enter Git username"
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<Text size="sm">Git Token</Text>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<PasswordInput
|
||||
value={gitToken}
|
||||
onChange={(event) =>
|
||||
onInputChange('gitToken', event.currentTarget.value)
|
||||
}
|
||||
disabled={!gitEnabled}
|
||||
placeholder="Enter Git token"
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<Text size="sm">Auto Commit</Text>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<Group justify="flex-end">
|
||||
<Switch
|
||||
checked={gitAutoCommit}
|
||||
onChange={(event) =>
|
||||
onInputChange('gitAutoCommit', event.currentTarget.checked)
|
||||
}
|
||||
disabled={!gitEnabled}
|
||||
/>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<Text size="sm">Commit Message Template</Text>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
value={gitCommitMsgTemplate}
|
||||
onChange={(event) =>
|
||||
onInputChange('gitCommitMsgTemplate', event.currentTarget.value)
|
||||
}
|
||||
disabled={!gitEnabled}
|
||||
placeholder="Enter commit message template"
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useState,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useMantineColorScheme } from '@mantine/core';
|
||||
import { fetchUserSettings, saveUserSettings } from '../services/api';
|
||||
import { DEFAULT_SETTINGS } from '../utils/constants';
|
||||
|
||||
@@ -13,6 +15,7 @@ const SettingsContext = createContext();
|
||||
export const useSettings = () => useContext(SettingsContext);
|
||||
|
||||
export const SettingsProvider = ({ children }) => {
|
||||
const { colorScheme, setColorScheme } = useMantineColorScheme();
|
||||
const [settings, setSettings] = useState(DEFAULT_SETTINGS);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -21,6 +24,7 @@ export const SettingsProvider = ({ children }) => {
|
||||
try {
|
||||
const userSettings = await fetchUserSettings(1);
|
||||
setSettings(userSettings.settings);
|
||||
setColorScheme(userSettings.settings.theme);
|
||||
} catch (error) {
|
||||
console.error('Failed to load user settings:', error);
|
||||
} finally {
|
||||
@@ -31,34 +35,40 @@ export const SettingsProvider = ({ children }) => {
|
||||
loadSettings();
|
||||
}, []);
|
||||
|
||||
const updateSettings = async (newSettings) => {
|
||||
try {
|
||||
await saveUserSettings({
|
||||
userId: 1,
|
||||
settings: newSettings,
|
||||
});
|
||||
setSettings(newSettings);
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
const updateSettings = useCallback(
|
||||
async (newSettings) => {
|
||||
try {
|
||||
await saveUserSettings({
|
||||
userId: 1,
|
||||
settings: newSettings,
|
||||
});
|
||||
setSettings(newSettings);
|
||||
if (newSettings.theme) {
|
||||
setColorScheme(newSettings.theme);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[setColorScheme]
|
||||
);
|
||||
|
||||
const updateTheme = (newTheme) => {
|
||||
setSettings((prevSettings) => ({
|
||||
...prevSettings,
|
||||
theme: newTheme,
|
||||
}));
|
||||
};
|
||||
const toggleColorScheme = useCallback(() => {
|
||||
const newTheme = colorScheme === 'dark' ? 'light' : 'dark';
|
||||
setColorScheme(newTheme);
|
||||
updateSettings({ ...settings, theme: newTheme });
|
||||
}, [colorScheme, settings, setColorScheme, updateSettings]);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
settings,
|
||||
updateSettings,
|
||||
updateTheme,
|
||||
toggleColorScheme,
|
||||
loading,
|
||||
colorScheme,
|
||||
}),
|
||||
[settings, loading]
|
||||
[settings, updateSettings, toggleColorScheme, loading, colorScheme]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { fetchFileList } from '../services/api';
|
||||
|
||||
export const useFileList = () => {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useToasts } from '@geist-ui/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { lookupFileByName } from '../services/api';
|
||||
import { DEFAULT_FILE } from '../utils/constants';
|
||||
|
||||
export const useFileNavigation = () => {
|
||||
const { setToast } = useToasts();
|
||||
|
||||
const [selectedFile, setSelectedFile] = useState(DEFAULT_FILE.path);
|
||||
const [isNewFile, setIsNewFile] = useState(true);
|
||||
|
||||
@@ -21,17 +19,22 @@ export const useFileNavigation = () => {
|
||||
if (filePaths.length >= 1) {
|
||||
handleFileSelect(filePaths[0]);
|
||||
} else {
|
||||
setToast({ text: `File "${filename}" not found`, type: 'error' });
|
||||
notifications.show({
|
||||
title: 'File Not Found',
|
||||
message: `File "${filename}" not found`,
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error looking up file:', error);
|
||||
setToast({
|
||||
text: 'Failed to lookup file.',
|
||||
type: 'error',
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to lookup file.',
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
},
|
||||
[handleFileSelect, setToast]
|
||||
[handleFileSelect]
|
||||
);
|
||||
|
||||
return { handleLinkClick, selectedFile, isNewFile, handleFileSelect };
|
||||
|
||||
@@ -1,54 +1,67 @@
|
||||
import { useCallback } from 'react';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { saveFileContent, deleteFile } from '../services/api';
|
||||
import { useToasts } from '@geist-ui/core';
|
||||
|
||||
export const useFileOperations = () => {
|
||||
const { setToast } = useToasts();
|
||||
const handleSave = useCallback(async (filePath, content) => {
|
||||
try {
|
||||
await saveFileContent(filePath, content);
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'File saved successfully',
|
||||
color: 'green',
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error saving file:', error);
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to save file',
|
||||
color: 'red',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSave = useCallback(
|
||||
async (filePath, content) => {
|
||||
try {
|
||||
await saveFileContent(filePath, content);
|
||||
setToast({ text: 'File saved successfully', type: 'success' });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error saving file:', error);
|
||||
setToast({ text: 'Failed to save file', type: 'error' });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[setToast]
|
||||
);
|
||||
const handleDelete = useCallback(async (filePath) => {
|
||||
try {
|
||||
await deleteFile(filePath);
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'File deleted successfully',
|
||||
color: 'green',
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error deleting file:', error);
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to delete file',
|
||||
color: 'red',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
async (filePath) => {
|
||||
try {
|
||||
await deleteFile(filePath);
|
||||
setToast({ text: 'File deleted successfully', type: 'success' });
|
||||
return true;
|
||||
} catch (error) {
|
||||
setToast({ text: `Error deleting file`, type: 'error' });
|
||||
console.error('Error deleting file:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[setToast]
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(
|
||||
async (fileName, initialContent = '') => {
|
||||
try {
|
||||
await saveFileContent(fileName, initialContent);
|
||||
setToast({ text: 'File created successfully', type: 'success' });
|
||||
return true;
|
||||
} catch (error) {
|
||||
setToast({ text: `Error creating new file`, type: 'error' });
|
||||
console.error('Error creating new file:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[setToast]
|
||||
);
|
||||
const handleCreate = useCallback(async (fileName, initialContent = '') => {
|
||||
try {
|
||||
await saveFileContent(fileName, initialContent);
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'File created successfully',
|
||||
color: 'green',
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error creating new file:', error);
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to create new file',
|
||||
color: 'red',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { handleSave, handleDelete, handleCreate };
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { pullChanges, commitAndPush } from '../services/api';
|
||||
|
||||
export const useGitOperations = (gitEnabled) => {
|
||||
@@ -6,11 +7,19 @@ export const useGitOperations = (gitEnabled) => {
|
||||
if (!gitEnabled) return false;
|
||||
try {
|
||||
await pullChanges();
|
||||
setToast({ text: 'Successfully pulled latest changes', type: 'success' });
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'Successfully pulled latest changes',
|
||||
color: 'green',
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to pull latest changes:', error);
|
||||
setToast({ text: 'Failed to pull latest changes', type: 'error' });
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to pull latest changes',
|
||||
color: 'red',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}, [gitEnabled]);
|
||||
@@ -20,14 +29,19 @@ export const useGitOperations = (gitEnabled) => {
|
||||
if (!gitEnabled) return false;
|
||||
try {
|
||||
await commitAndPush(message);
|
||||
setToast({
|
||||
text: 'Successfully committed and pushed changes',
|
||||
type: 'success',
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'Successfully committed and pushed changes',
|
||||
color: 'green',
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to commit and push changes:', error);
|
||||
setToast({ text: 'Failed to commit and push changes', type: 'error' });
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to commit and push changes',
|
||||
color: 'red',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -20,11 +20,11 @@ module.exports = (env, argv) => {
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: ['style-loader', 'css-loader', 'sass-loader'],
|
||||
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
use: ['style-loader', 'css-loader', 'postcss-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user