mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-06 07:54:22 +00:00
Implement state contexts
This commit is contained in:
@@ -1,48 +1,17 @@
|
|||||||
|
// App.js
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { GeistProvider, CssBaseline, Page, useToasts } from '@geist-ui/core';
|
import { GeistProvider, CssBaseline, Page } from '@geist-ui/core';
|
||||||
import Header from './components/Header';
|
import Header from './components/Header';
|
||||||
import MainContent from './components/MainContent';
|
import MainContent from './components/MainContent';
|
||||||
import { useFileManagement } from './hooks/useFileManagement';
|
|
||||||
import { SettingsProvider, useSettings } from './contexts/SettingsContext';
|
import { SettingsProvider, useSettings } from './contexts/SettingsContext';
|
||||||
import { lookupFileByName } from './services/api';
|
import { FileContentProvider } from './contexts/FileContentContext';
|
||||||
|
import { FileListProvider } from './contexts/FileListContext';
|
||||||
|
import { GitOperationsProvider } from './contexts/GitOperationsContext';
|
||||||
|
import { UIStateProvider } from './contexts/UIStateContext';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
|
|
||||||
function AppContent() {
|
function AppContent() {
|
||||||
const { settings, loading } = useSettings();
|
const { settings, loading } = useSettings();
|
||||||
const { setToast } = useToasts();
|
|
||||||
|
|
||||||
const {
|
|
||||||
content,
|
|
||||||
files,
|
|
||||||
selectedFile,
|
|
||||||
isNewFile,
|
|
||||||
hasUnsavedChanges,
|
|
||||||
error,
|
|
||||||
handleFileSelect,
|
|
||||||
handleContentChange,
|
|
||||||
handleSave,
|
|
||||||
pullLatestChanges,
|
|
||||||
} = useFileManagement(settings.gitEnabled);
|
|
||||||
|
|
||||||
const handleLinkClick = async (filename) => {
|
|
||||||
try {
|
|
||||||
const filePaths = await lookupFileByName(filename);
|
|
||||||
if (filePaths.length === 1) {
|
|
||||||
handleFileSelect(filePaths[0]);
|
|
||||||
} else if (filePaths.length > 1) {
|
|
||||||
// Handle multiple file options (you may want to show a modal or dropdown)
|
|
||||||
console.log('Multiple files found:', filePaths);
|
|
||||||
} else {
|
|
||||||
setToast({ text: `File "${filename}" not found`, type: 'error' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error looking up file:', error);
|
|
||||||
setToast({
|
|
||||||
text: 'Failed to lookup file. Please try again.',
|
|
||||||
type: 'error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
@@ -54,21 +23,7 @@ function AppContent() {
|
|||||||
<Page>
|
<Page>
|
||||||
<Header />
|
<Header />
|
||||||
<Page.Content className="page-content">
|
<Page.Content className="page-content">
|
||||||
<MainContent
|
<MainContent />
|
||||||
content={content}
|
|
||||||
files={files}
|
|
||||||
selectedFile={selectedFile}
|
|
||||||
isNewFile={isNewFile}
|
|
||||||
hasUnsavedChanges={hasUnsavedChanges}
|
|
||||||
error={error}
|
|
||||||
onFileSelect={handleFileSelect}
|
|
||||||
onContentChange={handleContentChange}
|
|
||||||
onSave={handleSave}
|
|
||||||
settings={settings}
|
|
||||||
pullLatestChanges={pullLatestChanges}
|
|
||||||
onLinkClick={handleLinkClick}
|
|
||||||
lookupFileByName={lookupFileByName}
|
|
||||||
/>
|
|
||||||
</Page.Content>
|
</Page.Content>
|
||||||
</Page>
|
</Page>
|
||||||
</GeistProvider>
|
</GeistProvider>
|
||||||
@@ -78,7 +33,15 @@ function AppContent() {
|
|||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<AppContent />
|
<FileListProvider>
|
||||||
|
<FileContentProvider>
|
||||||
|
<GitOperationsProvider>
|
||||||
|
<UIStateProvider>
|
||||||
|
<AppContent />
|
||||||
|
</UIStateProvider>
|
||||||
|
</GitOperationsProvider>
|
||||||
|
</FileContentProvider>
|
||||||
|
</FileListProvider>
|
||||||
</SettingsProvider>
|
</SettingsProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ import Editor from './Editor';
|
|||||||
import MarkdownPreview from './MarkdownPreview';
|
import MarkdownPreview from './MarkdownPreview';
|
||||||
import { getFileUrl } from '../services/api';
|
import { getFileUrl } from '../services/api';
|
||||||
import { isImageFile } from '../utils/fileHelpers';
|
import { isImageFile } from '../utils/fileHelpers';
|
||||||
|
import { useFileContentContext } from '../contexts/FileContentContext';
|
||||||
|
|
||||||
const ContentView = ({
|
const ContentView = ({
|
||||||
activeTab,
|
activeTab,
|
||||||
content,
|
|
||||||
selectedFile,
|
|
||||||
onContentChange,
|
|
||||||
onSave,
|
|
||||||
themeType,
|
themeType,
|
||||||
onLinkClick,
|
onLinkClick,
|
||||||
lookupFileByName,
|
lookupFileByName,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { content, selectedFile, handleContentChange, handleSave } =
|
||||||
|
useFileContentContext();
|
||||||
|
|
||||||
if (isImageFile(selectedFile)) {
|
if (isImageFile(selectedFile)) {
|
||||||
return (
|
return (
|
||||||
<div className="image-preview">
|
<div className="image-preview">
|
||||||
@@ -33,8 +33,8 @@ const ContentView = ({
|
|||||||
return activeTab === 'source' ? (
|
return activeTab === 'source' ? (
|
||||||
<Editor
|
<Editor
|
||||||
content={content}
|
content={content}
|
||||||
onChange={onContentChange}
|
onChange={handleContentChange}
|
||||||
onSave={onSave}
|
onSave={handleSave}
|
||||||
filePath={selectedFile}
|
filePath={selectedFile}
|
||||||
themeType={themeType}
|
themeType={themeType}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Tooltip, ButtonGroup, Spacer } from '@geist-ui/core';
|
import { Button, Tooltip, ButtonGroup, Spacer } from '@geist-ui/core';
|
||||||
import { Plus, Trash, GitPullRequest, GitCommit } from '@geist-ui/icons';
|
import { Plus, Trash, GitPullRequest, GitCommit } from '@geist-ui/icons';
|
||||||
|
import { useFileContentContext } from '../contexts/FileContentContext';
|
||||||
|
import { useGitOperationsContext } from '../contexts/GitOperationsContext';
|
||||||
|
import { useSettings } from '../contexts/SettingsContext';
|
||||||
|
import { useUIStateContext } from '../contexts/UIStateContext';
|
||||||
|
|
||||||
|
const FileActions = () => {
|
||||||
|
const { selectedFile } = useFileContentContext();
|
||||||
|
const { pullLatestChanges } = useGitOperationsContext();
|
||||||
|
const { settings } = useSettings();
|
||||||
|
const {
|
||||||
|
setNewFileModalVisible,
|
||||||
|
setDeleteFileModalVisible,
|
||||||
|
setCommitMessageModalVisible,
|
||||||
|
} = useUIStateContext();
|
||||||
|
|
||||||
|
const handleCreateFile = () => setNewFileModalVisible(true);
|
||||||
|
const handleDeleteFile = () => setDeleteFileModalVisible(true);
|
||||||
|
const handleCommitAndPush = () => setCommitMessageModalVisible(true);
|
||||||
|
|
||||||
const FileActions = ({
|
|
||||||
selectedFile,
|
|
||||||
gitEnabled,
|
|
||||||
gitAutoCommit,
|
|
||||||
onPull,
|
|
||||||
onCommitAndPush,
|
|
||||||
onCreateFile,
|
|
||||||
onDeleteFile,
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup className="file-actions">
|
<ButtonGroup className="file-actions">
|
||||||
<Tooltip text="Create new file" type="dark">
|
<Tooltip text="Create new file" type="dark">
|
||||||
@@ -18,7 +27,7 @@ const FileActions = ({
|
|||||||
icon={<Plus />}
|
icon={<Plus />}
|
||||||
auto
|
auto
|
||||||
scale={2 / 3}
|
scale={2 / 3}
|
||||||
onClick={onCreateFile}
|
onClick={handleCreateFile}
|
||||||
px={0.6}
|
px={0.6}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -31,7 +40,7 @@ const FileActions = ({
|
|||||||
icon={<Trash />}
|
icon={<Trash />}
|
||||||
auto
|
auto
|
||||||
scale={2 / 3}
|
scale={2 / 3}
|
||||||
onClick={onDeleteFile}
|
onClick={handleDeleteFile}
|
||||||
disabled={!selectedFile}
|
disabled={!selectedFile}
|
||||||
type="error"
|
type="error"
|
||||||
px={0.6}
|
px={0.6}
|
||||||
@@ -39,24 +48,28 @@ const FileActions = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Spacer w={0.5} />
|
<Spacer w={0.5} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
text={gitEnabled ? 'Pull changes from remote' : 'Git is not enabled'}
|
text={
|
||||||
|
settings.gitEnabled
|
||||||
|
? 'Pull changes from remote'
|
||||||
|
: 'Git is not enabled'
|
||||||
|
}
|
||||||
type="dark"
|
type="dark"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
icon={<GitPullRequest />}
|
icon={<GitPullRequest />}
|
||||||
auto
|
auto
|
||||||
scale={2 / 3}
|
scale={2 / 3}
|
||||||
onClick={onPull}
|
onClick={pullLatestChanges}
|
||||||
disabled={!gitEnabled}
|
disabled={!settings.gitEnabled}
|
||||||
px={0.6}
|
px={0.6}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Spacer w={0.5} />
|
<Spacer w={0.5} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
text={
|
text={
|
||||||
!gitEnabled
|
!settings.gitEnabled
|
||||||
? 'Git is not enabled'
|
? 'Git is not enabled'
|
||||||
: gitAutoCommit
|
: settings.gitAutoCommit
|
||||||
? 'Auto-commit is enabled'
|
? 'Auto-commit is enabled'
|
||||||
: 'Commit and push changes'
|
: 'Commit and push changes'
|
||||||
}
|
}
|
||||||
@@ -66,8 +79,8 @@ const FileActions = ({
|
|||||||
icon={<GitCommit />}
|
icon={<GitCommit />}
|
||||||
auto
|
auto
|
||||||
scale={2 / 3}
|
scale={2 / 3}
|
||||||
onClick={onCommitAndPush}
|
onClick={handleCommitAndPush}
|
||||||
disabled={!gitEnabled || gitAutoCommit}
|
disabled={!settings.gitEnabled || settings.gitAutoCommit}
|
||||||
px={0.6}
|
px={0.6}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Tree } from '@geist-ui/core';
|
import { Tree } from '@geist-ui/core';
|
||||||
import { File, Folder, Image } from '@geist-ui/icons';
|
import { File, Folder, Image } from '@geist-ui/icons';
|
||||||
|
import { useFileContentContext } from '../contexts/FileContentContext';
|
||||||
|
import { useFileListContext } from '../contexts/FileListContext';
|
||||||
|
import { isImageFile } from '../utils/fileHelpers';
|
||||||
|
|
||||||
|
const FileTree = () => {
|
||||||
|
const { files } = useFileListContext();
|
||||||
|
const { selectedFile, handleFileSelect } = useFileContentContext();
|
||||||
|
|
||||||
const FileTree = ({
|
|
||||||
files = [],
|
|
||||||
onFileSelect = () => {},
|
|
||||||
selectedFile = null,
|
|
||||||
}) => {
|
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
return <div>No files to display</div>;
|
return <div>No files to display</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelect = (filePath) => {
|
|
||||||
onFileSelect(filePath);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderLabel = (node) => {
|
const renderLabel = (node) => {
|
||||||
const path = node.extra;
|
const path = node.extra;
|
||||||
return (
|
return (
|
||||||
@@ -24,11 +22,6 @@ const FileTree = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isImageFile = (fileName) => {
|
|
||||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'];
|
|
||||||
return imageExtensions.some((ext) => fileName.toLowerCase().endsWith(ext));
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderIcon = ({ type, name }) => {
|
const renderIcon = ({ type, name }) => {
|
||||||
if (type === 'directory') return <Folder />;
|
if (type === 'directory') return <Folder />;
|
||||||
return isImageFile(name) ? <Image /> : <File />;
|
return isImageFile(name) ? <Image /> : <File />;
|
||||||
@@ -37,7 +30,7 @@ const FileTree = ({
|
|||||||
return (
|
return (
|
||||||
<Tree
|
<Tree
|
||||||
value={files}
|
value={files}
|
||||||
onClick={handleSelect}
|
onClick={(filePath) => handleFileSelect(filePath)}
|
||||||
renderIcon={renderIcon}
|
renderIcon={renderIcon}
|
||||||
renderLabel={renderLabel}
|
renderLabel={renderLabel}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
// components/MainContent.js
|
||||||
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Grid,
|
Grid,
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
@@ -10,46 +11,36 @@ import {
|
|||||||
import { Code, Eye } from '@geist-ui/icons';
|
import { Code, Eye } from '@geist-ui/icons';
|
||||||
import FileTree from './FileTree';
|
import FileTree from './FileTree';
|
||||||
import FileActions from './FileActions';
|
import FileActions from './FileActions';
|
||||||
|
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 ContentView from './ContentView';
|
import { useFileListContext } from '../contexts/FileListContext';
|
||||||
import { commitAndPush, saveFileContent, deleteFile } from '../services/api';
|
import { useFileContentContext } from '../contexts/FileContentContext';
|
||||||
import { isImageFile } from '../utils/fileHelpers';
|
import { useGitOperationsContext } from '../contexts/GitOperationsContext';
|
||||||
import { useSettings } from '../contexts/SettingsContext';
|
import { useUIStateContext } from '../contexts/UIStateContext';
|
||||||
|
import { useFileNavigation } from '../hooks/useFileNavigation';
|
||||||
|
|
||||||
const MainContent = ({
|
const MainContent = () => {
|
||||||
content,
|
const { files } = useFileListContext();
|
||||||
files,
|
const { selectedFile, hasUnsavedChanges } = useFileContentContext();
|
||||||
selectedFile,
|
const { pullLatestChanges } = useGitOperationsContext();
|
||||||
hasUnsavedChanges,
|
const {
|
||||||
onFileSelect,
|
activeTab,
|
||||||
onContentChange,
|
setActiveTab,
|
||||||
onSave,
|
newFileModalVisible,
|
||||||
pullLatestChanges,
|
setNewFileModalVisible,
|
||||||
onLinkClick,
|
deleteFileModalVisible,
|
||||||
lookupFileByName,
|
setDeleteFileModalVisible,
|
||||||
}) => {
|
commitMessageModalVisible,
|
||||||
const [activeTab, setActiveTab] = useState('source');
|
setCommitMessageModalVisible,
|
||||||
|
} = useUIStateContext();
|
||||||
|
const { handleLinkClick } = useFileNavigation();
|
||||||
const { type: themeType } = useTheme();
|
const { type: themeType } = useTheme();
|
||||||
const { setToast } = useToasts();
|
const { setToast } = useToasts();
|
||||||
const { settings } = useSettings();
|
|
||||||
const [newFileModalVisible, setNewFileModalVisible] = useState(false);
|
|
||||||
const [newFileName, setNewFileName] = useState('');
|
|
||||||
const [deleteFileModalVisible, setDeleteFileModalVisible] = useState(false);
|
|
||||||
const [commitMessageModalVisible, setCommitMessageModalVisible] =
|
|
||||||
useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isImageFile(selectedFile)) {
|
|
||||||
setActiveTab('preview');
|
|
||||||
}
|
|
||||||
}, [selectedFile]);
|
|
||||||
|
|
||||||
const handleTabChange = (value) => {
|
const handleTabChange = (value) => {
|
||||||
if (!isImageFile(selectedFile) || value === 'preview') {
|
setActiveTab(value);
|
||||||
setActiveTab(value);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePull = async () => {
|
const handlePull = async () => {
|
||||||
@@ -68,64 +59,14 @@ const MainContent = ({
|
|||||||
setNewFileModalVisible(true);
|
setNewFileModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNewFileSubmit = async () => {
|
|
||||||
if (newFileName) {
|
|
||||||
try {
|
|
||||||
await saveFileContent(newFileName, '');
|
|
||||||
setToast({ text: 'New file created successfully', type: 'success' });
|
|
||||||
await pullLatestChanges();
|
|
||||||
onFileSelect(newFileName);
|
|
||||||
} catch (error) {
|
|
||||||
setToast({
|
|
||||||
text: 'Failed to create new file: ' + error.message,
|
|
||||||
type: 'error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setNewFileModalVisible(false);
|
|
||||||
setNewFileName('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteFile = () => {
|
const handleDeleteFile = () => {
|
||||||
setDeleteFileModalVisible(true);
|
setDeleteFileModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmDeleteFile = async () => {
|
|
||||||
try {
|
|
||||||
await deleteFile(selectedFile);
|
|
||||||
setToast({ text: 'File deleted successfully', type: 'success' });
|
|
||||||
await pullLatestChanges();
|
|
||||||
onFileSelect(null);
|
|
||||||
} catch (error) {
|
|
||||||
setToast({
|
|
||||||
text: 'Failed to delete file: ' + error.message,
|
|
||||||
type: 'error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setDeleteFileModalVisible(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCommitAndPush = () => {
|
const handleCommitAndPush = () => {
|
||||||
setCommitMessageModalVisible(true);
|
setCommitMessageModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmCommitAndPush = async (message) => {
|
|
||||||
try {
|
|
||||||
await commitAndPush(message);
|
|
||||||
setToast({
|
|
||||||
text: 'Changes committed and pushed successfully',
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
await pullLatestChanges();
|
|
||||||
} catch (error) {
|
|
||||||
setToast({
|
|
||||||
text: 'Failed to commit and push changes: ' + error.message,
|
|
||||||
type: 'error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setCommitMessageModalVisible(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderBreadcrumbs = () => {
|
const renderBreadcrumbs = () => {
|
||||||
if (!selectedFile) return null;
|
if (!selectedFile) return null;
|
||||||
const pathParts = selectedFile.split('/');
|
const pathParts = selectedFile.split('/');
|
||||||
@@ -149,19 +90,12 @@ const MainContent = ({
|
|||||||
<Grid xs={24} sm={6} md={5} lg={4} height="100%" className="sidebar">
|
<Grid xs={24} sm={6} md={5} lg={4} height="100%" className="sidebar">
|
||||||
<div className="file-tree-container">
|
<div className="file-tree-container">
|
||||||
<FileActions
|
<FileActions
|
||||||
selectedFile={selectedFile}
|
|
||||||
gitEnabled={settings.gitEnabled}
|
|
||||||
gitAutoCommit={settings.gitAutoCommit}
|
|
||||||
onPull={handlePull}
|
onPull={handlePull}
|
||||||
onCommitAndPush={handleCommitAndPush}
|
onCommitAndPush={handleCommitAndPush}
|
||||||
onCreateFile={handleCreateFile}
|
onCreateFile={handleCreateFile}
|
||||||
onDeleteFile={handleDeleteFile}
|
onDeleteFile={handleDeleteFile}
|
||||||
/>
|
/>
|
||||||
<FileTree
|
<FileTree files={files} />
|
||||||
files={files}
|
|
||||||
onFileSelect={onFileSelect}
|
|
||||||
selectedFile={selectedFile}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid
|
<Grid
|
||||||
@@ -175,46 +109,22 @@ const MainContent = ({
|
|||||||
<div className="content-header">
|
<div className="content-header">
|
||||||
{renderBreadcrumbs()}
|
{renderBreadcrumbs()}
|
||||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||||
<Tabs.Item
|
<Tabs.Item label={<Code />} value="source" />
|
||||||
label={<Code />}
|
|
||||||
value="source"
|
|
||||||
disabled={isImageFile(selectedFile)}
|
|
||||||
/>
|
|
||||||
<Tabs.Item label={<Eye />} value="preview" />
|
<Tabs.Item label={<Eye />} value="preview" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
<div className="content-body">
|
<div className="content-body">
|
||||||
<ContentView
|
<ContentView
|
||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
content={content}
|
|
||||||
selectedFile={selectedFile}
|
|
||||||
onContentChange={onContentChange}
|
|
||||||
onSave={onSave}
|
|
||||||
themeType={themeType}
|
themeType={themeType}
|
||||||
onLinkClick={onLinkClick}
|
onLinkClick={handleLinkClick}
|
||||||
lookupFileByName={lookupFileByName}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid.Container>
|
</Grid.Container>
|
||||||
<CreateFileModal
|
<CreateFileModal visible={newFileModalVisible} />
|
||||||
visible={newFileModalVisible}
|
<DeleteFileModal visible={deleteFileModalVisible} />
|
||||||
onClose={() => setNewFileModalVisible(false)}
|
<CommitMessageModal visible={commitMessageModalVisible} />
|
||||||
onSubmit={handleNewFileSubmit}
|
|
||||||
fileName={newFileName}
|
|
||||||
setFileName={setNewFileName}
|
|
||||||
/>
|
|
||||||
<DeleteFileModal
|
|
||||||
visible={deleteFileModalVisible}
|
|
||||||
onClose={() => setDeleteFileModalVisible(false)}
|
|
||||||
onConfirm={confirmDeleteFile}
|
|
||||||
fileName={selectedFile}
|
|
||||||
/>
|
|
||||||
<CommitMessageModal
|
|
||||||
visible={commitMessageModalVisible}
|
|
||||||
onClose={() => setCommitMessageModalVisible(false)}
|
|
||||||
onSubmit={confirmCommitAndPush}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Modal, Input } from '@geist-ui/core';
|
import { Modal, Input } from '@geist-ui/core';
|
||||||
|
import { useGitOperationsContext } from '../../contexts/GitOperationsContext';
|
||||||
|
import { useUIStateContext } from '../../contexts/UIStateContext';
|
||||||
|
|
||||||
const CommitMessageModal = ({ visible, onClose, onSubmit }) => {
|
const CommitMessageModal = () => {
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
|
const { handleCommitAndPush } = useGitOperationsContext();
|
||||||
|
const { commitMessageModalVisible, setCommitMessageModalVisible } =
|
||||||
|
useUIStateContext();
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = async () => {
|
||||||
onSubmit(message);
|
if (message) {
|
||||||
setMessage('');
|
await handleCommitAndPush(message);
|
||||||
|
setMessage('');
|
||||||
|
setCommitMessageModalVisible(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible={visible} onClose={onClose}>
|
<Modal
|
||||||
|
visible={commitMessageModalVisible}
|
||||||
|
onClose={() => setCommitMessageModalVisible(false)}
|
||||||
|
>
|
||||||
<Modal.Title>Enter Commit Message</Modal.Title>
|
<Modal.Title>Enter Commit Message</Modal.Title>
|
||||||
<Modal.Content>
|
<Modal.Content>
|
||||||
<Input
|
<Input
|
||||||
@@ -20,7 +31,7 @@ const CommitMessageModal = ({ visible, onClose, onSubmit }) => {
|
|||||||
onChange={(e) => setMessage(e.target.value)}
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
<Modal.Action passive onClick={onClose}>
|
<Modal.Action passive onClick={() => setCommitMessageModalVisible(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Modal.Action>
|
</Modal.Action>
|
||||||
<Modal.Action onClick={handleSubmit}>Commit</Modal.Action>
|
<Modal.Action onClick={handleSubmit}>Commit</Modal.Action>
|
||||||
|
|||||||
@@ -1,15 +1,26 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Modal, Input } from '@geist-ui/core';
|
import { Modal, Input } from '@geist-ui/core';
|
||||||
|
import { useFileContentContext } from '../../contexts/FileContentContext';
|
||||||
|
import { useUIStateContext } from '../../contexts/UIStateContext';
|
||||||
|
|
||||||
|
const CreateFileModal = () => {
|
||||||
|
const [fileName, setFileName] = useState('');
|
||||||
|
const { newFileModalVisible, setNewFileModalVisible } = useUIStateContext();
|
||||||
|
const { handleCreateNewFile } = useFileContentContext();
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (fileName) {
|
||||||
|
await handleCreateNewFile(fileName);
|
||||||
|
setFileName('');
|
||||||
|
setNewFileModalVisible(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const CreateFileModal = ({
|
|
||||||
visible,
|
|
||||||
onClose,
|
|
||||||
onSubmit,
|
|
||||||
fileName,
|
|
||||||
setFileName,
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<Modal visible={visible} onClose={onClose}>
|
<Modal
|
||||||
|
visible={newFileModalVisible}
|
||||||
|
onClose={() => setNewFileModalVisible(false)}
|
||||||
|
>
|
||||||
<Modal.Title>Create New File</Modal.Title>
|
<Modal.Title>Create New File</Modal.Title>
|
||||||
<Modal.Content>
|
<Modal.Content>
|
||||||
<Input
|
<Input
|
||||||
@@ -19,10 +30,10 @@ const CreateFileModal = ({
|
|||||||
onChange={(e) => setFileName(e.target.value)}
|
onChange={(e) => setFileName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
<Modal.Action passive onClick={onClose}>
|
<Modal.Action passive onClick={() => setNewFileModalVisible(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Modal.Action>
|
</Modal.Action>
|
||||||
<Modal.Action onClick={onSubmit}>Create</Modal.Action>
|
<Modal.Action onClick={handleSubmit}>Create</Modal.Action>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,31 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Modal, Text } from '@geist-ui/core';
|
import { Modal, Text } from '@geist-ui/core';
|
||||||
|
import { useFileContentContext } from '../../contexts/FileContentContext';
|
||||||
|
import { useUIStateContext } from '../../contexts/UIStateContext';
|
||||||
|
|
||||||
|
const DeleteFileModal = () => {
|
||||||
|
const { selectedFile, handleDeleteFile } = useFileContentContext();
|
||||||
|
const { deleteFileModalVisible, setDeleteFileModalVisible } =
|
||||||
|
useUIStateContext();
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
await handleDeleteFile();
|
||||||
|
setDeleteFileModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
const DeleteFileModal = ({ visible, onClose, onConfirm, fileName }) => {
|
|
||||||
return (
|
return (
|
||||||
<Modal visible={visible} onClose={onClose}>
|
<Modal
|
||||||
|
visible={deleteFileModalVisible}
|
||||||
|
onClose={() => setDeleteFileModalVisible(false)}
|
||||||
|
>
|
||||||
<Modal.Title>Delete File</Modal.Title>
|
<Modal.Title>Delete File</Modal.Title>
|
||||||
<Modal.Content>
|
<Modal.Content>
|
||||||
<Text>Are you sure you want to delete "{fileName}"?</Text>
|
<Text>Are you sure you want to delete "{selectedFile}"?</Text>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
<Modal.Action passive onClick={onClose}>
|
<Modal.Action passive onClick={() => setDeleteFileModalVisible(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Modal.Action>
|
</Modal.Action>
|
||||||
<Modal.Action onClick={onConfirm}>Delete</Modal.Action>
|
<Modal.Action onClick={handleConfirm}>Delete</Modal.Action>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
24
frontend/src/contexts/FileContentContext.js
Normal file
24
frontend/src/contexts/FileContentContext.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React, { createContext, useContext } from 'react';
|
||||||
|
import { useFileContent } from '../hooks/useFileContent';
|
||||||
|
|
||||||
|
const FileContentContext = createContext();
|
||||||
|
|
||||||
|
export const FileContentProvider = ({ children }) => {
|
||||||
|
const fileContentHook = useFileContent();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileContentContext.Provider value={fileContentHook}>
|
||||||
|
{children}
|
||||||
|
</FileContentContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFileContentContext = () => {
|
||||||
|
const context = useContext(FileContentContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useFileContentContext must be used within a FileContentProvider'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
24
frontend/src/contexts/FileListContext.js
Normal file
24
frontend/src/contexts/FileListContext.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React, { createContext, useContext } from 'react';
|
||||||
|
import { useFileList } from '../hooks/useFileList';
|
||||||
|
|
||||||
|
const FileListContext = createContext();
|
||||||
|
|
||||||
|
export const FileListProvider = ({ children }) => {
|
||||||
|
const fileListHook = useFileList();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileListContext.Provider value={fileListHook}>
|
||||||
|
{children}
|
||||||
|
</FileListContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFileListContext = () => {
|
||||||
|
const context = useContext(FileListContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useFileListContext must be used within a FileListProvider'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
24
frontend/src/contexts/GitOperationsContext.js
Normal file
24
frontend/src/contexts/GitOperationsContext.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React, { createContext, useContext } from 'react';
|
||||||
|
import { useGitOperations } from '../hooks/useGitOperations';
|
||||||
|
|
||||||
|
const GitOperationsContext = createContext();
|
||||||
|
|
||||||
|
export const GitOperationsProvider = ({ children }) => {
|
||||||
|
const gitOperationsHook = useGitOperations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GitOperationsContext.Provider value={gitOperationsHook}>
|
||||||
|
{children}
|
||||||
|
</GitOperationsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGitOperationsContext = () => {
|
||||||
|
const context = useContext(GitOperationsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useGitOperationsContext must be used within a GitOperationsProvider'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
34
frontend/src/contexts/UIStateContext.js
Normal file
34
frontend/src/contexts/UIStateContext.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import React, { createContext, useContext, useState } from 'react';
|
||||||
|
|
||||||
|
const UIStateContext = createContext();
|
||||||
|
|
||||||
|
export const UIStateProvider = ({ children }) => {
|
||||||
|
const [activeTab, setActiveTab] = useState('source');
|
||||||
|
const [newFileModalVisible, setNewFileModalVisible] = useState(false);
|
||||||
|
const [deleteFileModalVisible, setDeleteFileModalVisible] = useState(false);
|
||||||
|
const [commitMessageModalVisible, setCommitMessageModalVisible] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
activeTab,
|
||||||
|
setActiveTab,
|
||||||
|
newFileModalVisible,
|
||||||
|
setNewFileModalVisible,
|
||||||
|
deleteFileModalVisible,
|
||||||
|
setDeleteFileModalVisible,
|
||||||
|
commitMessageModalVisible,
|
||||||
|
setCommitMessageModalVisible,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UIStateContext.Provider value={value}>{children}</UIStateContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUIStateContext = () => {
|
||||||
|
const context = useContext(UIStateContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useUIStateContext must be used within a UIStateProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { fetchFileContent, saveFileContent } from '../services/api';
|
import { fetchFileContent, saveFileContent, deleteFile } from '../services/api';
|
||||||
import { isImageFile } from '../utils/fileHelpers';
|
import { isImageFile } from '../utils/fileHelpers';
|
||||||
|
import { useToasts } from '@geist-ui/core';
|
||||||
|
import { useFileListContext } from '../contexts/FileListContext';
|
||||||
|
|
||||||
const DEFAULT_FILE = {
|
const DEFAULT_FILE = {
|
||||||
name: 'New File.md',
|
name: 'New File.md',
|
||||||
@@ -14,16 +16,12 @@ export const useFileContent = () => {
|
|||||||
const [isNewFile, setIsNewFile] = useState(true);
|
const [isNewFile, setIsNewFile] = useState(true);
|
||||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const { setToast } = useToasts();
|
||||||
|
const { loadFileList } = useFileListContext();
|
||||||
|
|
||||||
const handleFileSelect = useCallback(
|
const handleFileSelect = useCallback(
|
||||||
async (filePath) => {
|
async (filePath) => {
|
||||||
if (hasUnsavedChanges) {
|
console.log('handleFileSelect', filePath);
|
||||||
const confirmSwitch = window.confirm(
|
|
||||||
'You have unsaved changes. Are you sure you want to switch files?'
|
|
||||||
);
|
|
||||||
if (!confirmSwitch) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (filePath === DEFAULT_FILE.path) {
|
if (filePath === DEFAULT_FILE.path) {
|
||||||
setContent(DEFAULT_FILE.content);
|
setContent(DEFAULT_FILE.content);
|
||||||
@@ -67,13 +65,38 @@ export const useFileContent = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const createNewFile = useCallback(() => {
|
const handleCreateNewFile = useCallback(
|
||||||
setContent(DEFAULT_FILE.content);
|
async (fileName, initialContent = '') => {
|
||||||
setSelectedFile(DEFAULT_FILE.path);
|
try {
|
||||||
setIsNewFile(true);
|
await saveFileContent(fileName, initialContent);
|
||||||
setHasUnsavedChanges(false);
|
setToast({ text: 'New file created successfully', type: 'success' });
|
||||||
setError(null);
|
await loadFileList(); // Refresh the file list
|
||||||
}, []);
|
handleFileSelect(fileName);
|
||||||
|
} catch (error) {
|
||||||
|
setToast({
|
||||||
|
text: 'Failed to create new file: ' + error.message,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setToast, loadFileList, handleFileSelect]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeleteFile = useCallback(async () => {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
try {
|
||||||
|
await deleteFile(selectedFile);
|
||||||
|
setToast({ text: 'File deleted successfully', type: 'success' });
|
||||||
|
await loadFileList(); // Refresh the file list
|
||||||
|
setSelectedFile(null);
|
||||||
|
setContent('');
|
||||||
|
} catch (error) {
|
||||||
|
setToast({
|
||||||
|
text: 'Failed to delete file: ' + error.message,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [selectedFile, setToast, loadFileList]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content,
|
content,
|
||||||
@@ -84,7 +107,8 @@ export const useFileContent = () => {
|
|||||||
handleFileSelect,
|
handleFileSelect,
|
||||||
handleContentChange,
|
handleContentChange,
|
||||||
handleSave,
|
handleSave,
|
||||||
createNewFile,
|
handleCreateNewFile,
|
||||||
|
handleDeleteFile,
|
||||||
DEFAULT_FILE,
|
DEFAULT_FILE,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
39
frontend/src/hooks/useFileNavigation.js
Normal file
39
frontend/src/hooks/useFileNavigation.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// hooks/useFileNavigation.js
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useToasts } from '@geist-ui/core';
|
||||||
|
import { lookupFileByName } from '../services/api';
|
||||||
|
import { useFileContentContext } from '../contexts/FileContentContext';
|
||||||
|
|
||||||
|
export const useFileNavigation = () => {
|
||||||
|
const { setToast } = useToasts();
|
||||||
|
const { handleFileSelect } = useFileContentContext();
|
||||||
|
|
||||||
|
const handleLinkClick = useCallback(
|
||||||
|
async (filename) => {
|
||||||
|
try {
|
||||||
|
const filePaths = await lookupFileByName(filename);
|
||||||
|
if (filePaths.length === 1) {
|
||||||
|
handleFileSelect(filePaths[0]);
|
||||||
|
} else if (filePaths.length > 1) {
|
||||||
|
// Handle multiple file options (you may want to show a modal or dropdown)
|
||||||
|
console.log('Multiple files found:', filePaths);
|
||||||
|
setToast({
|
||||||
|
text: 'Multiple files found with the same name. Please specify the full path.',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setToast({ text: `File "${filename}" not found`, type: 'error' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error looking up file:', error);
|
||||||
|
setToast({
|
||||||
|
text: 'Failed to lookup file. Please try again.',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleFileSelect, setToast]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { handleLinkClick };
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user