mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-06 16:04:23 +00:00
Implement Settings context
This commit is contained in:
@@ -1,31 +1,16 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React from 'react';
|
||||||
import { GeistProvider, CssBaseline, Page, useToasts } from '@geist-ui/core';
|
import { GeistProvider, CssBaseline, Page, useToasts } 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 { useFileManagement } from './hooks/useFileManagement';
|
||||||
import { fetchUserSettings, lookupFileByName } from './services/api';
|
import { SettingsProvider, useSettings } from './contexts/SettingsContext';
|
||||||
|
import { lookupFileByName } from './services/api';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
|
|
||||||
function App() {
|
function AppContent() {
|
||||||
const [themeType, setThemeType] = useState('light');
|
const { settings, loading } = useSettings();
|
||||||
const [userId, setUserId] = useState(1);
|
|
||||||
const [settings, setSettings] = useState({ gitEnabled: false });
|
|
||||||
const { setToast } = useToasts();
|
const { setToast } = useToasts();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadUserSettings = async () => {
|
|
||||||
try {
|
|
||||||
const fetchedSettings = await fetchUserSettings(userId);
|
|
||||||
setSettings(fetchedSettings.settings);
|
|
||||||
setThemeType(fetchedSettings.settings.theme);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load user settings:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadUserSettings();
|
|
||||||
}, [userId]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
content,
|
content,
|
||||||
files,
|
files,
|
||||||
@@ -39,18 +24,14 @@ function App() {
|
|||||||
pullLatestChanges,
|
pullLatestChanges,
|
||||||
} = useFileManagement(settings.gitEnabled);
|
} = useFileManagement(settings.gitEnabled);
|
||||||
|
|
||||||
const handleThemeChange = (newTheme) => {
|
|
||||||
setThemeType(newTheme);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLinkClick = async (filename) => {
|
const handleLinkClick = async (filename) => {
|
||||||
try {
|
try {
|
||||||
const filePaths = await lookupFileByName(filename);
|
const filePaths = await lookupFileByName(filename);
|
||||||
if (filePaths.length === 1) {
|
if (filePaths.length === 1) {
|
||||||
handleFileSelect(filePaths[0]);
|
handleFileSelect(filePaths[0]);
|
||||||
} else if (filePaths.length > 1) {
|
} else if (filePaths.length > 1) {
|
||||||
setFileOptions(filePaths.map((path) => ({ label: path, value: path })));
|
// Handle multiple file options (you may want to show a modal or dropdown)
|
||||||
setFileSelectionModalVisible(true);
|
console.log('Multiple files found:', filePaths);
|
||||||
} else {
|
} else {
|
||||||
setToast({ text: `File "${filename}" not found`, type: 'error' });
|
setToast({ text: `File "${filename}" not found`, type: 'error' });
|
||||||
}
|
}
|
||||||
@@ -63,11 +44,15 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GeistProvider themeType={themeType}>
|
<GeistProvider themeType={settings.theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Page>
|
<Page>
|
||||||
<Header currentTheme={themeType} onThemeChange={handleThemeChange} />
|
<Header />
|
||||||
<Page.Content className="page-content">
|
<Page.Content className="page-content">
|
||||||
<MainContent
|
<MainContent
|
||||||
content={content}
|
content={content}
|
||||||
@@ -90,4 +75,12 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<SettingsProvider>
|
||||||
|
<AppContent />
|
||||||
|
</SettingsProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import React, { useState } from 'react';
|
|||||||
import { Page, Text, User, Button, Spacer } from '@geist-ui/core';
|
import { Page, Text, User, Button, Spacer } from '@geist-ui/core';
|
||||||
import { Settings as SettingsIcon } from '@geist-ui/icons';
|
import { Settings as SettingsIcon } from '@geist-ui/icons';
|
||||||
import Settings from './Settings';
|
import Settings from './Settings';
|
||||||
|
import { useSettings } from '../contexts/SettingsContext';
|
||||||
|
|
||||||
const Header = ({ currentTheme, onThemeChange }) => {
|
const Header = () => {
|
||||||
const [settingsVisible, setSettingsVisible] = useState(false);
|
const [settingsVisible, setSettingsVisible] = useState(false);
|
||||||
|
const { settings } = useSettings();
|
||||||
|
|
||||||
const openSettings = () => setSettingsVisible(true);
|
const openSettings = () => setSettingsVisible(true);
|
||||||
const closeSettings = () => setSettingsVisible(false);
|
const closeSettings = () => setSettingsVisible(false);
|
||||||
@@ -16,12 +18,7 @@ const Header = ({ currentTheme, onThemeChange }) => {
|
|||||||
<User src="https://via.placeholder.com/40" name="User" />
|
<User src="https://via.placeholder.com/40" name="User" />
|
||||||
<Spacer w={0.5} />
|
<Spacer w={0.5} />
|
||||||
<Button auto icon={<SettingsIcon />} onClick={openSettings} />
|
<Button auto icon={<SettingsIcon />} onClick={openSettings} />
|
||||||
<Settings
|
<Settings visible={settingsVisible} onClose={closeSettings} />
|
||||||
visible={settingsVisible}
|
|
||||||
onClose={closeSettings}
|
|
||||||
currentTheme={currentTheme}
|
|
||||||
onThemeChange={onThemeChange}
|
|
||||||
/>
|
|
||||||
</Page.Header>
|
</Page.Header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import CommitMessageModal from './modals/CommitMessageModal';
|
|||||||
import ContentView from './ContentView';
|
import ContentView from './ContentView';
|
||||||
import { commitAndPush, saveFileContent, deleteFile } from '../services/api';
|
import { commitAndPush, saveFileContent, deleteFile } from '../services/api';
|
||||||
import { isImageFile } from '../utils/fileHelpers';
|
import { isImageFile } from '../utils/fileHelpers';
|
||||||
|
import { useSettings } from '../contexts/SettingsContext';
|
||||||
|
|
||||||
const MainContent = ({
|
const MainContent = ({
|
||||||
content,
|
content,
|
||||||
@@ -25,7 +26,6 @@ const MainContent = ({
|
|||||||
onFileSelect,
|
onFileSelect,
|
||||||
onContentChange,
|
onContentChange,
|
||||||
onSave,
|
onSave,
|
||||||
settings,
|
|
||||||
pullLatestChanges,
|
pullLatestChanges,
|
||||||
onLinkClick,
|
onLinkClick,
|
||||||
lookupFileByName,
|
lookupFileByName,
|
||||||
@@ -33,6 +33,7 @@ const MainContent = ({
|
|||||||
const [activeTab, setActiveTab] = useState('source');
|
const [activeTab, setActiveTab] = useState('source');
|
||||||
const { type: themeType } = useTheme();
|
const { type: themeType } = useTheme();
|
||||||
const { setToast } = useToasts();
|
const { setToast } = useToasts();
|
||||||
|
const { settings } = useSettings();
|
||||||
const [newFileModalVisible, setNewFileModalVisible] = useState(false);
|
const [newFileModalVisible, setNewFileModalVisible] = useState(false);
|
||||||
const [newFileName, setNewFileName] = useState('');
|
const [newFileName, setNewFileName] = useState('');
|
||||||
const [deleteFileModalVisible, setDeleteFileModalVisible] = useState(false);
|
const [deleteFileModalVisible, setDeleteFileModalVisible] = useState(false);
|
||||||
|
|||||||
@@ -1,75 +1,88 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useReducer, useEffect, useCallback, useRef } from 'react';
|
||||||
import { Modal, Spacer, useTheme, Dot, useToasts } from '@geist-ui/core';
|
import { Modal, Spacer, useTheme, Dot, useToasts } from '@geist-ui/core';
|
||||||
import { saveUserSettings, fetchUserSettings } from '../services/api';
|
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';
|
||||||
import GitSettings from './settings/GitSettings';
|
import GitSettings from './settings/GitSettings';
|
||||||
|
|
||||||
const Settings = ({ visible, onClose, currentTheme, onThemeChange }) => {
|
const initialState = {
|
||||||
const theme = useTheme();
|
localSettings: {},
|
||||||
const { setToast } = useToasts();
|
initialSettings: {},
|
||||||
const [settings, setSettings] = useState({
|
hasUnsavedChanges: false,
|
||||||
autoSave: false,
|
};
|
||||||
gitEnabled: false,
|
|
||||||
gitUrl: '',
|
|
||||||
gitUser: '',
|
|
||||||
gitToken: '',
|
|
||||||
gitAutoCommit: false,
|
|
||||||
gitCommitMsgTemplate: '',
|
|
||||||
});
|
|
||||||
const [themeSettings, setThemeSettings] = useState(currentTheme);
|
|
||||||
const [originalSettings, setOriginalSettings] = useState({});
|
|
||||||
const [originalTheme, setOriginalTheme] = useState(currentTheme);
|
|
||||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
||||||
const [isInitialized, setIsInitialized] = useState(false);
|
|
||||||
|
|
||||||
const loadSettings = useCallback(async () => {
|
function settingsReducer(state, action) {
|
||||||
try {
|
console.log('Reducer action:', action.type, action.payload); // Debug log
|
||||||
const userSettings = await fetchUserSettings(1); // Assuming user ID 1 for now
|
switch (action.type) {
|
||||||
const { theme, ...otherSettings } = userSettings.settings;
|
case 'INIT_SETTINGS':
|
||||||
setSettings(otherSettings);
|
return {
|
||||||
setThemeSettings(theme);
|
...state,
|
||||||
setOriginalSettings(otherSettings);
|
localSettings: action.payload,
|
||||||
setOriginalTheme(theme);
|
initialSettings: action.payload,
|
||||||
setHasUnsavedChanges(false);
|
hasUnsavedChanges: false,
|
||||||
setIsInitialized(true);
|
};
|
||||||
} catch (error) {
|
case 'UPDATE_LOCAL_SETTINGS':
|
||||||
console.error('Failed to load user settings:', error);
|
const newLocalSettings = { ...state.localSettings, ...action.payload };
|
||||||
|
const hasChanges =
|
||||||
|
JSON.stringify(newLocalSettings) !==
|
||||||
|
JSON.stringify(state.initialSettings);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
localSettings: newLocalSettings,
|
||||||
|
hasUnsavedChanges: hasChanges,
|
||||||
|
};
|
||||||
|
case 'MARK_SAVED':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
initialSettings: state.localSettings,
|
||||||
|
hasUnsavedChanges: false,
|
||||||
|
};
|
||||||
|
case 'RESET':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
localSettings: state.initialSettings,
|
||||||
|
hasUnsavedChanges: false,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Settings = ({ visible, onClose }) => {
|
||||||
|
const { settings, updateSettings, updateTheme } = useSettings();
|
||||||
|
const { setToast } = useToasts();
|
||||||
|
const [state, dispatch] = useReducer(settingsReducer, initialState);
|
||||||
|
const isInitialMount = useRef(true);
|
||||||
|
const updateThemeTimeoutRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInitialMount.current) {
|
||||||
|
isInitialMount.current = false;
|
||||||
|
dispatch({ type: 'INIT_SETTINGS', payload: settings });
|
||||||
|
}
|
||||||
|
}, [settings]);
|
||||||
|
|
||||||
|
const handleInputChange = useCallback((key, value) => {
|
||||||
|
dispatch({ type: 'UPDATE_LOCAL_SETTINGS', payload: { [key]: value } });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleThemeChange = useCallback(() => {
|
||||||
if (!isInitialized) {
|
const newTheme = state.localSettings.theme === 'dark' ? 'light' : 'dark';
|
||||||
loadSettings();
|
dispatch({ type: 'UPDATE_LOCAL_SETTINGS', payload: { theme: newTheme } });
|
||||||
|
|
||||||
|
// Debounce the theme update
|
||||||
|
if (updateThemeTimeoutRef.current) {
|
||||||
|
clearTimeout(updateThemeTimeoutRef.current);
|
||||||
}
|
}
|
||||||
}, [isInitialized, loadSettings]);
|
updateThemeTimeoutRef.current = setTimeout(() => {
|
||||||
|
updateTheme(newTheme);
|
||||||
useEffect(() => {
|
}, 0);
|
||||||
const settingsChanged =
|
}, [state.localSettings.theme, updateTheme]);
|
||||||
JSON.stringify(settings) !== JSON.stringify(originalSettings);
|
|
||||||
const themeChanged = themeSettings !== originalTheme;
|
|
||||||
setHasUnsavedChanges(settingsChanged || themeChanged);
|
|
||||||
}, [settings, themeSettings, originalSettings, originalTheme]);
|
|
||||||
|
|
||||||
const handleInputChange = (key, value) => {
|
|
||||||
setSettings((prev) => ({ ...prev, [key]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleThemeChange = () => {
|
|
||||||
const newTheme = themeSettings === 'dark' ? 'light' : 'dark';
|
|
||||||
setThemeSettings(newTheme);
|
|
||||||
onThemeChange(newTheme);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
await saveUserSettings({
|
await updateSettings(state.localSettings);
|
||||||
userId: 1, // Assuming user ID 1 for now
|
dispatch({ type: 'MARK_SAVED' });
|
||||||
settings: { ...settings, theme: themeSettings },
|
|
||||||
});
|
|
||||||
setOriginalSettings(settings);
|
|
||||||
setOriginalTheme(themeSettings);
|
|
||||||
setHasUnsavedChanges(false);
|
|
||||||
setToast({ text: 'Settings saved successfully', type: 'success' });
|
setToast({ text: 'Settings saved successfully', type: 'success' });
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -81,36 +94,66 @@ const Settings = ({ visible, onClose, currentTheme, onThemeChange }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
if (state.hasUnsavedChanges) {
|
||||||
|
const confirmClose = window.confirm(
|
||||||
|
'You have unsaved changes. Are you sure you want to close without saving?'
|
||||||
|
);
|
||||||
|
if (confirmClose) {
|
||||||
|
updateTheme(state.initialSettings.theme); // Revert theme if not saved
|
||||||
|
dispatch({ type: 'RESET' });
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
state.hasUnsavedChanges,
|
||||||
|
state.initialSettings.theme,
|
||||||
|
updateTheme,
|
||||||
|
onClose,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (updateThemeTimeoutRef.current) {
|
||||||
|
clearTimeout(updateThemeTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
console.log('State:', state); // Debugging log
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible={visible} onClose={onClose}>
|
<Modal visible={visible} onClose={handleClose}>
|
||||||
<Modal.Title>
|
<Modal.Title>
|
||||||
Settings
|
Settings
|
||||||
{hasUnsavedChanges && (
|
{state.hasUnsavedChanges && (
|
||||||
<Dot type="warning" style={{ marginLeft: '8px' }} />
|
<Dot type="warning" style={{ marginLeft: '8px' }} />
|
||||||
)}
|
)}
|
||||||
</Modal.Title>
|
</Modal.Title>
|
||||||
<Modal.Content>
|
<Modal.Content>
|
||||||
<AppearanceSettings
|
<AppearanceSettings
|
||||||
themeSettings={themeSettings}
|
themeSettings={state.localSettings.theme}
|
||||||
onThemeChange={handleThemeChange}
|
onThemeChange={handleThemeChange}
|
||||||
/>
|
/>
|
||||||
<Spacer h={1} />
|
<Spacer h={1} />
|
||||||
<EditorSettings
|
<EditorSettings
|
||||||
autoSave={settings.autoSave}
|
autoSave={state.localSettings.autoSave}
|
||||||
onAutoSaveChange={(value) => handleInputChange('autoSave', value)}
|
onAutoSaveChange={(value) => handleInputChange('autoSave', value)}
|
||||||
/>
|
/>
|
||||||
<Spacer h={1} />
|
<Spacer h={1} />
|
||||||
<GitSettings
|
<GitSettings
|
||||||
gitEnabled={settings.gitEnabled}
|
gitEnabled={state.localSettings.gitEnabled}
|
||||||
gitUrl={settings.gitUrl}
|
gitUrl={state.localSettings.gitUrl}
|
||||||
gitUser={settings.gitUser}
|
gitUser={state.localSettings.gitUser}
|
||||||
gitToken={settings.gitToken}
|
gitToken={state.localSettings.gitToken}
|
||||||
gitAutoCommit={settings.gitAutoCommit}
|
gitAutoCommit={state.localSettings.gitAutoCommit}
|
||||||
gitCommitMsgTemplate={settings.gitCommitMsgTemplate}
|
gitCommitMsgTemplate={state.localSettings.gitCommitMsgTemplate}
|
||||||
onInputChange={handleInputChange}
|
onInputChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
<Modal.Action passive onClick={onClose}>
|
<Modal.Action passive onClick={handleClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Modal.Action>
|
</Modal.Action>
|
||||||
<Modal.Action onClick={handleSubmit}>Save Changes</Modal.Action>
|
<Modal.Action onClick={handleSubmit}>Save Changes</Modal.Action>
|
||||||
|
|||||||
66
frontend/src/contexts/SettingsContext.js
Normal file
66
frontend/src/contexts/SettingsContext.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import React, { createContext, useState, useContext, useEffect } from 'react';
|
||||||
|
import { fetchUserSettings, saveUserSettings } from '../services/api';
|
||||||
|
|
||||||
|
const SettingsContext = createContext();
|
||||||
|
|
||||||
|
export const useSettings = () => {
|
||||||
|
return useContext(SettingsContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsProvider = ({ children }) => {
|
||||||
|
const [settings, setSettings] = useState({
|
||||||
|
theme: 'light',
|
||||||
|
autoSave: false,
|
||||||
|
gitEnabled: false,
|
||||||
|
gitUrl: '',
|
||||||
|
gitUser: '',
|
||||||
|
gitToken: '',
|
||||||
|
gitAutoCommit: false,
|
||||||
|
gitCommitMsgTemplate: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadSettings = async () => {
|
||||||
|
try {
|
||||||
|
const userSettings = await fetchUserSettings(1); // Assuming user ID 1 for now
|
||||||
|
setSettings(userSettings.settings);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load user settings:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadSettings();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateSettings = async (newSettings) => {
|
||||||
|
try {
|
||||||
|
await saveUserSettings({
|
||||||
|
userId: 1, // Assuming user ID 1 for now
|
||||||
|
settings: newSettings,
|
||||||
|
});
|
||||||
|
setSettings(newSettings);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save settings:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTheme = (newTheme) => {
|
||||||
|
setSettings((prevSettings) => ({
|
||||||
|
...prevSettings,
|
||||||
|
theme: newTheme,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsContext.Provider
|
||||||
|
value={{ settings, updateSettings, updateTheme, loading }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SettingsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user