Merge pull request #3 from LordMathis/feat/mantine-ui

Feat/mantine UI
This commit is contained in:
2024-10-12 11:10:05 +02:00
committed by GitHub
27 changed files with 2394 additions and 1061 deletions

View File

@@ -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

View File

@@ -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"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View 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',
},
},
},
};

View File

@@ -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>
</>
); );
} }

View File

@@ -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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View 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;

View File

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

View File

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

View 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = () => {

View File

@@ -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 };

View File

@@ -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 };
}; };

View File

@@ -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;
} }
}, },

View File

@@ -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'],
}, },
], ],
}, },