Initial commit: Nextcloud Notes Tauri app with WYSIWYG editor
Features: - WYSIWYG markdown editor with TipTap - Manual save with unsaved changes indicator - Auto-title derivation from first line (with manual override) - Manual sync button with visual feedback - Auto-sync every 5 minutes - Full formatting toolbar (bold, italic, headings, lists, code) - Note creation, editing, deletion - Search and favorites filter - Cross-platform desktop app built with Tauri + React + TypeScript
This commit is contained in:
153
src/App.tsx
Normal file
153
src/App.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { LoginView } from './components/LoginView';
|
||||
import { NotesList } from './components/NotesList';
|
||||
import { NoteEditor } from './components/NoteEditor';
|
||||
import { NextcloudAPI } from './api/nextcloud';
|
||||
import { Note } from './types';
|
||||
|
||||
function App() {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [api, setApi] = useState<NextcloudAPI | null>(null);
|
||||
const [notes, setNotes] = useState<Note[]>([]);
|
||||
const [selectedNoteId, setSelectedNoteId] = useState<number | null>(null);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false);
|
||||
const [fontSize] = useState(14);
|
||||
|
||||
useEffect(() => {
|
||||
const savedServer = localStorage.getItem('serverURL');
|
||||
const savedUsername = localStorage.getItem('username');
|
||||
const savedPassword = localStorage.getItem('password');
|
||||
|
||||
if (savedServer && savedUsername && savedPassword) {
|
||||
const apiInstance = new NextcloudAPI({
|
||||
serverURL: savedServer,
|
||||
username: savedUsername,
|
||||
password: savedPassword,
|
||||
});
|
||||
setApi(apiInstance);
|
||||
setIsLoggedIn(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (api && isLoggedIn) {
|
||||
syncNotes();
|
||||
const interval = setInterval(syncNotes, 300000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [api, isLoggedIn]);
|
||||
|
||||
const syncNotes = async () => {
|
||||
if (!api) return;
|
||||
try {
|
||||
const fetched = await api.fetchNotes();
|
||||
setNotes(fetched.sort((a, b) => b.modified - a.modified));
|
||||
if (!selectedNoteId && fetched.length > 0) {
|
||||
setSelectedNoteId(fetched[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Sync failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogin = (serverURL: string, username: string, password: string) => {
|
||||
localStorage.setItem('serverURL', serverURL);
|
||||
localStorage.setItem('username', username);
|
||||
localStorage.setItem('password', password);
|
||||
|
||||
const apiInstance = new NextcloudAPI({ serverURL, username, password });
|
||||
setApi(apiInstance);
|
||||
setIsLoggedIn(true);
|
||||
};
|
||||
|
||||
const handleCreateNote = async () => {
|
||||
if (!api) return;
|
||||
try {
|
||||
const timestamp = new Date().toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
}).replace(/[/:]/g, '-').replace(', ', ' ');
|
||||
|
||||
const note = await api.createNote(`New Note ${timestamp}`, '', '');
|
||||
setNotes([note, ...notes]);
|
||||
setSelectedNoteId(note.id);
|
||||
} catch (error) {
|
||||
console.error('Create note failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateNote = async (updatedNote: Note) => {
|
||||
if (!api) return;
|
||||
try {
|
||||
console.log('Sending to API - content length:', updatedNote.content.length);
|
||||
console.log('Sending to API - last 50 chars:', updatedNote.content.slice(-50));
|
||||
const result = await api.updateNote(updatedNote);
|
||||
console.log('Received from API - content length:', result.content.length);
|
||||
console.log('Received from API - last 50 chars:', result.content.slice(-50));
|
||||
// Update notes array with server response now that we have manual save
|
||||
setNotes(notes.map(n => n.id === result.id ? result : n));
|
||||
} catch (error) {
|
||||
console.error('Update note failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteNote = async (note: Note) => {
|
||||
if (!api) return;
|
||||
if (!confirm(`Delete "${note.title}"?`)) return;
|
||||
|
||||
try {
|
||||
await api.deleteNote(note.id);
|
||||
setNotes(notes.filter(n => n.id !== note.id));
|
||||
if (selectedNoteId === note.id) {
|
||||
setSelectedNoteId(notes[0]?.id || null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete note failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredNotes = notes.filter(note => {
|
||||
if (showFavoritesOnly && !note.favorite) return false;
|
||||
if (searchText) {
|
||||
const search = searchText.toLowerCase();
|
||||
return note.title.toLowerCase().includes(search) ||
|
||||
note.content.toLowerCase().includes(search);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const selectedNote = notes.find(n => n.id === selectedNoteId) || null;
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <LoginView onLogin={handleLogin} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<NotesList
|
||||
notes={filteredNotes}
|
||||
selectedNoteId={selectedNoteId}
|
||||
onSelectNote={setSelectedNoteId}
|
||||
onCreateNote={handleCreateNote}
|
||||
onDeleteNote={handleDeleteNote}
|
||||
onSync={syncNotes}
|
||||
searchText={searchText}
|
||||
onSearchChange={setSearchText}
|
||||
showFavoritesOnly={showFavoritesOnly}
|
||||
onToggleFavorites={() => setShowFavoritesOnly(!showFavoritesOnly)}
|
||||
/>
|
||||
<NoteEditor
|
||||
note={selectedNote}
|
||||
onUpdateNote={handleUpdateNote}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
Reference in New Issue
Block a user