diff --git a/package-lock.json b/package-lock.json index decda87..9dd8d16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@tauri-apps/api": "^1.5.3", + "date-fns": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1" @@ -1447,6 +1448,16 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", diff --git a/package.json b/package.json index 325640c..89fb22c 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@tauri-apps/api": "^1.5.3", + "date-fns": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 605c058..55e6579 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -12,7 +12,6 @@ async fn read_file_content(path: String) -> Result { fs::read_to_string(path) .map_err(|e| e.to_string()) } - #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9a3f54f..12773da 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "CleanType", - "version": "0.1.0" + "version": "0.2.0" }, "tauri": { "allowlist": { diff --git a/src/App.module.css b/src/App.module.css new file mode 100644 index 0000000..6f0cf6d --- /dev/null +++ b/src/App.module.css @@ -0,0 +1,36 @@ +.app { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + overflow: hidden; + transition: all 0.15s ease; +} + +.darkTheme { + background-color: #1a1a1a; + color: rgba(255, 255, 255, 0.9); + --bg-color: #1a1a1a; + --text-color: rgba(255, 255, 255, 0.9); + --nav-bg: rgba(0, 0, 0, 0.85); + --border-color: rgba(255, 255, 255, 0.1); +} + +.lightTheme { + background-color: rgb(255, 252, 242); + color: rgba(0, 0, 0, 0.9); + --bg-color: rgb(255, 252, 242); + --text-color: rgba(0, 0, 0, 0.9); + --nav-bg: rgba(255, 252, 242, 0.95); + --border-color: rgba(0, 0, 0, 0.1); +} + +/* Mobile styles */ +@media (max-width: 768px) { + .app { + padding: 0; + } +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 3e14788..8980d6a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,14 +1,49 @@ +import { useState, useEffect } from 'react'; import { FullscreenEditor } from "./components/FullscreenEditor"; import { ErrorBoundary } from "./components/ErrorBoundary"; +import KeyboardShortcuts from './components/KeyboardShortcuts'; +import styles from './App.module.css'; function App() { + const [isDarkTheme, setIsDarkTheme] = useState(() => { + const saved = localStorage.getItem('cleantype-theme'); + return saved !== 'light'; // Default to dark theme if not explicitly set to light + }); + + const [isShortcutsOpen, setIsShortcutsOpen] = useState(false); + + // Save theme changes to localStorage + useEffect(() => { + localStorage.setItem('cleantype-theme', isDarkTheme ? 'dark' : 'light'); + }, [isDarkTheme]); + + useEffect(() => { + const handleKeyPress = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === '/') { + e.preventDefault(); + setIsShortcutsOpen(prev => !prev); + } + }; + + window.addEventListener('keydown', handleKeyPress); + return () => window.removeEventListener('keydown', handleKeyPress); + }, []); + return ( -
- +
+ setIsDarkTheme(prev => !prev)} + /> + setIsShortcutsOpen(false)} + isDarkTheme={isDarkTheme} + />
); } -export default App; +export default App; \ No newline at end of file diff --git a/src/components/ConfirmDialog.module.css b/src/components/ConfirmDialog.module.css new file mode 100644 index 0000000..8ae910f --- /dev/null +++ b/src/components/ConfirmDialog.module.css @@ -0,0 +1,114 @@ +.overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 2000; + backdrop-filter: blur(4px); +} + +.dialog { + background: rgba(30, 30, 30, 0.95); + border-radius: 12px; + padding: 24px; + min-width: 320px; + max-width: 90%; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + animation: dialogAppear 0.2s ease; +} + +.dialog h3 { + margin: 0 0 12px; + font-size: 18px; + font-weight: 500; + color: rgba(255, 255, 255, 0.9); +} + +.dialog p { + margin: 0 0 24px; + font-size: 15px; + line-height: 1.5; + color: rgba(255, 255, 255, 0.7); +} + +.actions { + display: flex; + justify-content: flex-end; + gap: 12px; +} + +.cancelButton, +.confirmButton { + all: unset; + cursor: pointer; + padding: 8px 16px; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + transition: all 0.2s ease; +} + +.cancelButton { + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.8); +} + +.cancelButton:hover { + background: rgba(255, 255, 255, 0.15); +} + +.confirmButton { + background: rgba(255, 59, 48, 0.15); + color: #ff3b30; +} + +.confirmButton:hover { + background: rgba(255, 59, 48, 0.25); +} + +/* Light theme */ +.lightTheme { + background: rgba(255, 255, 255, 0.95); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); +} + +.lightTheme h3 { + color: rgba(0, 0, 0, 0.9); +} + +.lightTheme p { + color: rgba(0, 0, 0, 0.7); +} + +.lightTheme .cancelButton { + background: rgba(0, 0, 0, 0.05); + color: rgba(0, 0, 0, 0.8); +} + +.lightTheme .cancelButton:hover { + background: rgba(0, 0, 0, 0.1); +} + +.lightTheme .confirmButton { + background: rgba(255, 59, 48, 0.1); +} + +.lightTheme .confirmButton:hover { + background: rgba(255, 59, 48, 0.15); +} + +@keyframes dialogAppear { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx new file mode 100644 index 0000000..9dc4268 --- /dev/null +++ b/src/components/ConfirmDialog.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import styles from './ConfirmDialog.module.css'; + +interface ConfirmDialogProps { + isOpen: boolean; + title: string; + message: string; + onConfirm: () => void; + onCancel: () => void; + isDarkTheme: boolean; +} + +export const ConfirmDialog: React.FC = ({ + isOpen, + title, + message, + onConfirm, + onCancel, + isDarkTheme, +}) => { + if (!isOpen) return null; + + return ( +
+
+

{title}

+

{message}

+
+ + +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/FullscreenEditor.module.css b/src/components/FullscreenEditor.module.css index 0fddc18..2e47192 100644 --- a/src/components/FullscreenEditor.module.css +++ b/src/components/FullscreenEditor.module.css @@ -10,7 +10,7 @@ display: flex; flex-direction: column; overflow: hidden; - background-color: #ffffff; + background-color: var(--bg-color); } .editorContainer { @@ -24,7 +24,7 @@ align-items: center; justify-content: center; background: var(--bg-color); - transition: background-color 0.3s ease; + transition: all 0.15s ease; overflow: hidden; } @@ -51,8 +51,7 @@ display: flex; align-items: center; justify-content: space-between; - background: var(--nav-bg); - border-top: 1px solid var(--border-color); + background: transparent; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; opacity: 0.85; transition: opacity 0.2s ease; @@ -86,29 +85,115 @@ border-radius: 4px; } -.timer { +.navDot { + color: #666; + opacity: 0.5; + user-select: none; + font-size: 15px; + margin: 0 2px; +} + +.fontButton, +.saveButton { + all: unset; + cursor: pointer; + padding: 4px 12px; + border-radius: 4px; + font-size: 15px; + opacity: 0.85; + transition: all 0.2s ease; display: flex; align-items: center; - gap: 6px; - min-width: 80px; - justify-content: center; - font-variant-numeric: tabular-nums; - opacity: 0.9; + gap: 4px; + background: transparent; } -.timer.active { - color: #4a9eff; +.fontButton:hover, +.sizeButton:hover, +.historyToggle:hover, +.themeToggle:hover, +.saveButton:hover { opacity: 1; } +.darkTheme { + background-color: #1a1a1a; +} + +.lightTheme { + background-color: rgb(255, 252, 242); +} + +.darkTheme .editor { + color: rgba(255, 255, 255, 0.9); + caret-color: rgba(255, 255, 255, 0.9); +} + .darkTheme .bottomNav { color: rgba(255, 255, 255, 0.8); } +.lightTheme .editor { + background: rgba(255, 252, 242, 0.95); + color: #333; + caret-color: #333; +} + .lightTheme .bottomNav { color: rgba(0, 0, 0, 0.85); } +.topControls { + position: fixed; + top: 2rem; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 16px; + z-index: 100; +} + +.wordCount { + position: fixed; + top: 2rem; + left: 50%; + transform: translateX(-50%); + font-size: 14px; + opacity: 0.8; + transition: opacity 0.2s ease; + font-family: 'Inter', system-ui, -apple-system, sans-serif; + padding: 6px 14px; + border-radius: 6px; + margin: 0; + z-index: 10; + background: transparent; + pointer-events: none; +} + +.darkTheme .wordCount { + color: rgba(255, 255, 255, 0.85); +} + +.lightTheme .wordCount { + color: rgba(0, 0, 0, 0.85); +} + +.timer { + display: flex; + align-items: center; + gap: 6px; + min-width: 80px; + justify-content: center; + font-variant-numeric: tabular-nums; + opacity: 0.9; +} + +.timer.active { + color: #4a9eff; + opacity: 1; +} + .fontInfo { display: flex; align-items: center; @@ -178,16 +263,8 @@ text-align: center; } -.navDot { - color: #666; - opacity: 0.5; - user-select: none; - font-size: 15px; -} - .navButton, -.fontOption, -.sizeButton { +.fontOption { all: unset; cursor: pointer; padding: 4px 8px; @@ -196,9 +273,9 @@ } .navButton:hover, -.fontOption:hover, -.sizeButton:hover { - background: rgba(255, 255, 255, 0.1); +.fontOption:hover { + background: transparent; + opacity: 1; } .sizeWrapper { @@ -463,7 +540,7 @@ } .fontButton:hover { - background: rgba(255, 255, 255, 0.1); + background: transparent; opacity: 1; } @@ -547,7 +624,7 @@ } .lightTheme { - background-color: #ffffff; + background-color: rgb(255, 252, 242); } .lightTheme .editor { @@ -573,45 +650,46 @@ color: rgba(255, 255, 255, 0.6); } -.wordCount { +.newEntryButton { position: fixed; top: 2rem; - left: 50%; - transform: translateX(-50%); - font-size: 14px; - opacity: 0.6; - transition: opacity 0.2s ease; - z-index: 100; - font-family: 'Inter', system-ui, -apple-system, sans-serif; - background: var(--bg-color); - padding: 6px 14px; - border-radius: 6px; - backdrop-filter: blur(8px); - margin-bottom: 2rem; + right: 2rem; + width: 36px; + height: 36px; + border-radius: 8px; + border: none; + background: transparent; + color: inherit; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + opacity: 0.7; + transition: all 0.2s ease; + z-index: 10; } -.wordCount:hover { +.newEntryButton:hover { opacity: 1; + transform: translateY(-1px); } -.editor::selection { - background-color: rgba(100, 100, 255, 0.2); -} - -.darkTheme .editor::selection { - background-color: rgba(255, 255, 255, 0.2); -} - -.lightTheme .editor::selection { - background-color: rgba(0, 0, 0, 0.1); +.darkTheme .newEntryButton { + color: rgba(255, 255, 255, 0.9); } -.lightTheme .wordCount { - color: rgba(0, 0, 0, 0.5); +.lightTheme .newEntryButton { + color: rgba(0, 0, 0, 0.9); } -.darkTheme .wordCount { - color: rgba(255, 255, 255, 0.6); +/* Remove the old wordCount positioning */ +.wordCount { + position: relative; + top: auto; + left: auto; + transform: none; + margin-bottom: 0; } /* Tablet and smaller desktop */ @@ -659,8 +737,8 @@ } .saveButton:hover { + background: transparent; opacity: 1; - background: rgba(255, 255, 255, 0.1); } .saveButton.hasChanges { @@ -670,4 +748,281 @@ .lightTheme .saveButton.hasChanges { color: #0066cc; +} + +.historyToggle { + padding: 4px 12px; + border-radius: 4px; + background: transparent; + border: none; + cursor: pointer; + font-size: 15px; + opacity: 0.85; + transition: opacity 0.2s ease; +} + +.historyToggle:hover { + opacity: 1; +} + +.historyToggle.active { + opacity: 1; +} + +.darkTheme .historyToggle { + color: rgba(255, 255, 255, 0.85); +} + +.lightTheme .historyToggle { + color: rgba(0, 0, 0, 0.85); +} + +.timerPopup button { + background: transparent; + border: none; + color: inherit; + padding: 4px 12px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + opacity: 0.8; +} + +.timerPopup button:hover { + opacity: 1; + background: rgba(255, 255, 255, 0.1); +} + +.timerPopup input { + background: rgba(255, 255, 255, 0.1); + border: none; + color: inherit; + padding: 4px 8px; + border-radius: 4px; + width: 60px; + font-size: 14px; +} + +.timerPopup input:focus { + outline: 1px solid rgba(255, 255, 255, 0.3); +} + +.timerPopup .startButton { + background: #4a9eff; + opacity: 1; +} + +.timerPopup .startButton:hover { + background: #3b8be6; +} + +.historyPanel { + position: fixed; + top: 0; + right: 0; + width: 320px; + height: 100vh; + background: var(--bg); + box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1); + padding: 2rem; + display: flex; + flex-direction: column; + gap: 1rem; + z-index: 1000; + animation: slideIn 0.2s ease; +} + +.darkTheme .historyPanel { + background: #1a1a1a; + box-shadow: -2px 0 10px rgba(0, 0, 0, 0.3); +} + +.lightTheme .historyPanel { + background: #fff; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + } + to { + transform: translateX(0); + } +} + +.historyHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.historyTitle { + font-size: 1.2rem; + font-weight: 500; +} + +.closeButton { + background: transparent; + border: none; + cursor: pointer; + padding: 4px; + opacity: 0.7; + transition: opacity 0.2s ease; +} + +.closeButton:hover { + opacity: 1; +} + +.historyList { + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.historyEntry { + padding: 1rem; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s ease; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.darkTheme .historyEntry { + background: rgba(255, 255, 255, 0.05); +} + +.darkTheme .historyEntry:hover { + background: rgba(255, 255, 255, 0.1); +} + +.lightTheme .historyEntry { + background: rgba(0, 0, 0, 0.05); +} + +.lightTheme .historyEntry:hover { + background: rgba(0, 0, 0, 0.1); +} + +.historyEntry.current { + border: 1px solid var(--accent-color); +} + +.entryPreview { + font-size: 0.9rem; + opacity: 0.7; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.entryMeta { + font-size: 0.8rem; + opacity: 0.5; + display: flex; + justify-content: space-between; +} + +.historyActions { + display: flex; + gap: 0.5rem; + margin-top: 1rem; +} + +.historyAction { + flex: 1; + padding: 0.5rem; + border-radius: 4px; + border: none; + cursor: pointer; + font-size: 0.9rem; + transition: opacity 0.2s ease; + background: transparent; +} + +.darkTheme .historyAction { + color: rgba(255, 255, 255, 0.85); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.lightTheme .historyAction { + color: rgba(0, 0, 0, 0.85); + border: 1px solid rgba(0, 0, 0, 0.2); +} + +.historyAction:hover { + opacity: 0.8; +} + +.deleteButton { + background: transparent; + border: none; + cursor: pointer; + padding: 4px; + opacity: 0.5; + transition: opacity 0.2s ease; +} + +.deleteButton:hover { + opacity: 0.8; +} + +.timerWrapper { + position: relative; + display: flex; + align-items: center; +} + +.timerWrapper :global(.timerContainer) { + min-width: 120px; +} + +.timerWrapper :global(.presetButtons) { + position: absolute; + top: 100%; + right: 0; + margin-top: 8px; + background: var(--nav-bg); + border-radius: 8px; + padding: 12px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + gap: 8px; + z-index: 10; +} + +.timerWrapper :global(.customInput) { + position: absolute; + top: 100%; + right: 0; + margin-top: 8px; + background: var(--nav-bg); + border-radius: 8px; + padding: 16px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); + z-index: 10; + min-width: 200px; +} + +.timerWrapper :global(.customTimeInput) { + width: 100%; + margin-bottom: 12px; +} + +.timerWrapper :global(.customButtons) { + display: flex; + gap: 8px; +} + +.timerWrapper :global(.customSubmit), +.timerWrapper :global(.customCancel) { + flex: 1; } \ No newline at end of file diff --git a/src/components/FullscreenEditor.tsx b/src/components/FullscreenEditor.tsx index a758f08..6e545b8 100644 --- a/src/components/FullscreenEditor.tsx +++ b/src/components/FullscreenEditor.tsx @@ -3,14 +3,17 @@ import styles from './FullscreenEditor.module.css'; import { toast, Toaster } from 'react-hot-toast'; import { Timer } from './Timer'; import { appWindow } from '@tauri-apps/api/window'; -import { save } from '@tauri-apps/api/dialog'; -import { writeTextFile } from '@tauri-apps/api/fs'; +import { HistoryPanel } from './HistoryPanel'; + +interface FullscreenEditorProps { + isDarkTheme: boolean; + onThemeToggle: () => void; +} const STORAGE_KEY = 'freewrite-content'; const FONT_STORAGE_KEY = 'freewrite-font'; const SIZE_STORAGE_KEY = 'freewrite-size'; const ENTRIES_KEY = 'freewrite-entries'; -const THEME_KEY = 'freewrite-theme'; interface Entry { id: string; @@ -46,7 +49,7 @@ const FONT_CATEGORIES = { special: ['random'] as FontStyle[] }; -export const FullscreenEditor: React.FC = () => { +export const FullscreenEditor: React.FC = ({ isDarkTheme, onThemeToggle }) => { const [content, setContent] = useState(() => { return localStorage.getItem(STORAGE_KEY) || ''; }); @@ -69,9 +72,6 @@ export const FullscreenEditor: React.FC = () => { }); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - const [isDarkTheme, setIsDarkTheme] = useState(() => { - return localStorage.getItem(THEME_KEY) !== 'light'; - }); const [entries, setEntries] = useState(() => { return JSON.parse(localStorage.getItem(ENTRIES_KEY) || '[]'); @@ -88,13 +88,14 @@ export const FullscreenEditor: React.FC = () => { const textareaRef = useRef(null); const [wordCount, setWordCount] = useState(0); - const [timeLeft, setTimeLeft] = useState(15 * 60); + const [timeLeft, setTimeLeft] = useState(0); const [isTimerActive, setIsTimerActive] = useState(false); - const [showTimerPopup, setShowTimerPopup] = useState(false); const [undoStack, setUndoStack] = useState([]); const [redoStack, setRedoStack] = useState([]); + const [isHistoryOpen, setIsHistoryOpen] = useState(false); + // Load entries when component mounts useEffect(() => { const savedEntries = localStorage.getItem(ENTRIES_KEY); @@ -137,36 +138,35 @@ export const FullscreenEditor: React.FC = () => { }, [content, currentEntry]); // Manual save with notification (Ctrl + S) - const saveCurrentEntry = async () => { + const handleSave = async () => { if (currentEntry && content.trim()) { - const updatedEntry: Entry = { - ...currentEntry, - content, - updatedAt: new Date().toISOString(), - font: currentFont, - fontSize: fontSize, - theme: isDarkTheme ? 'dark' : 'light' as Theme - }; - - const newEntries = [...entries]; - const index = newEntries.findIndex(e => e.id === currentEntry.id); - if (index >= 0) { - newEntries[index] = updatedEntry; - } else { - newEntries.push(updatedEntry); + try { + const updatedEntry: Entry = { + ...currentEntry, + content, + updatedAt: new Date().toISOString(), + font: currentFont, + fontSize: fontSize, + theme: isDarkTheme ? 'dark' : 'light' + }; + + const newEntries = entries.filter(e => e.id !== currentEntry.id); + newEntries.unshift(updatedEntry); + setEntries(newEntries); + localStorage.setItem(ENTRIES_KEY, JSON.stringify(newEntries)); + setHasUnsavedChanges(false); + + toast.success('Saved to history', { + duration: 2000, + style: { + background: isDarkTheme ? 'rgba(0, 0, 0, 0.85)' : 'rgba(255, 255, 255, 0.95)', + color: isDarkTheme ? '#fff' : '#333', + } + }); + } catch (error) { + console.error('Failed to save:', error); + toast.error('Failed to save to history'); } - setEntries(newEntries); - localStorage.setItem(ENTRIES_KEY, JSON.stringify(newEntries)); - localStorage.setItem(STORAGE_KEY, content); - setHasUnsavedChanges(false); - toast.success('Saved to history', { - duration: 2000, - icon: '💾', - style: { - background: isDarkTheme ? 'rgba(0, 0, 0, 0.85)' : '#ffffff', - color: isDarkTheme ? '#fff' : '#333', - } - }); } }; @@ -215,57 +215,42 @@ export const FullscreenEditor: React.FC = () => { }; const toggleTheme = () => { - setIsDarkTheme(prev => { - const newTheme = !prev; - localStorage.setItem(THEME_KEY, newTheme ? 'dark' : 'light'); - return newTheme; - }); + onThemeToggle(); }; const createNewEntry = () => { - const createEntry = () => { - const newEntry: Entry = { - id: Date.now().toString(), - content: '', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - title: 'Untitled', - font: currentFont, - fontSize: fontSize, - theme: isDarkTheme ? 'dark' : 'light' - }; - setCurrentEntry(newEntry); - setContent(''); - setHasUnsavedChanges(false); - - // Save the new entry immediately - const newEntries = [...entries, newEntry]; - setEntries(newEntries); - localStorage.setItem(ENTRIES_KEY, JSON.stringify(newEntries)); - - toast.success('New entry created'); + const newEntry: Entry = { + id: Date.now().toString(), + content: '', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + title: 'Untitled', + font: currentFont, + fontSize: fontSize, + theme: isDarkTheme ? 'dark' : 'light' }; - - if (hasUnsavedChanges && content.trim()) { - const shouldSave = window.confirm( - 'You have unsaved changes. Would you like to save before creating a new entry?' - ); - - if (shouldSave) { - saveCurrentEntry().then(createEntry); - } else { - createEntry(); + + setCurrentEntry(newEntry); + setContent(''); + setHasUnsavedChanges(false); + + const newEntries = [newEntry, ...entries]; + setEntries(newEntries); + localStorage.setItem(ENTRIES_KEY, JSON.stringify(newEntries)); + toast.success('New entry created', { + duration: 2000, + style: { + background: isDarkTheme ? 'rgba(0, 0, 0, 0.85)' : '#ffffff', + color: isDarkTheme ? '#fff' : '#333', } - } else { - createEntry(); - } + }); }; const handleKeyDown = (e: React.KeyboardEvent) => { - // Save (Ctrl + S) + // Save to history (Ctrl + S) if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault(); - saveCurrentEntry(); + handleSave(); return; } @@ -435,24 +420,25 @@ export const FullscreenEditor: React.FC = () => { const startTimer = (minutes: number) => { setTimeLeft(minutes * 60); setIsTimerActive(true); - setShowTimerPopup(false); toast.success(`Timer started: ${minutes} minutes`); }; const pauseTimer = () => { setIsTimerActive(false); - setShowTimerPopup(false); + toast.success('Timer paused'); }; const resumeTimer = () => { - setIsTimerActive(true); - setShowTimerPopup(false); + if (timeLeft > 0) { + setIsTimerActive(true); + toast.success('Timer resumed'); + } }; const resetTimer = () => { - setTimeLeft(15 * 60); + setTimeLeft(0); setIsTimerActive(false); - setShowTimerPopup(false); + toast.success('Timer reset'); }; // Random font button @@ -466,41 +452,41 @@ export const FullscreenEditor: React.FC = () => { setCurrentFont(fonts[newIndex]); }; - // Add save functionality - const handleSave = async () => { - try { - // First, show the save dialog to get the file path - const filePath = await save({ - filters: [{ - name: 'Text', - extensions: ['txt'] - }] - }); - - if (filePath) { - // Save the content to the selected file - await writeTextFile(filePath, content); - setHasUnsavedChanges(false); - toast.success('File saved successfully'); - } - } catch (error) { - console.error('Failed to save:', error); - toast.error('Failed to save file'); - } + const handleHistoryToggle = () => { + setIsHistoryOpen(prev => !prev); }; - // Add prompt before closing + // Auto-save effect useEffect(() => { - const handleBeforeUnload = (e: BeforeUnloadEvent) => { - if (hasUnsavedChanges) { - e.preventDefault(); - e.returnValue = ''; + const saveTimeout = setTimeout(() => { + if (currentEntry && content.trim()) { + const updatedEntry: Entry = { + ...currentEntry, + content, + updatedAt: new Date().toISOString(), + font: currentFont, + fontSize: fontSize, + theme: isDarkTheme ? 'dark' : 'light' + }; + + const newEntries = entries.filter(e => e.id !== currentEntry.id); + newEntries.unshift(updatedEntry); + setEntries(newEntries); + localStorage.setItem(ENTRIES_KEY, JSON.stringify(newEntries)); + localStorage.setItem(STORAGE_KEY, content); + setHasUnsavedChanges(false); } - }; + }, 300); // Even faster auto-save - window.addEventListener('beforeunload', handleBeforeUnload); - return () => window.removeEventListener('beforeunload', handleBeforeUnload); - }, [hasUnsavedChanges]); + return () => clearTimeout(saveTimeout); + }, [content, currentEntry, currentFont, fontSize, isDarkTheme]); + + // Simplified content change handler + const handleContentChange = (e: React.ChangeEvent) => { + const newContent = e.target.value; + setContent(newContent); + setHasUnsavedChanges(true); + }; return (
@@ -517,14 +503,18 @@ export const FullscreenEditor: React.FC = () => {
{wordCount} {wordCount === 1 ? 'word' : 'words'}
+