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