Implement Settings context

This commit is contained in:
2024-10-03 22:20:43 +02:00
parent 93e675e0c7
commit d21233a67f
5 changed files with 206 additions and 106 deletions

View File

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

View File

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

View File

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

View File

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

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