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:
drelich
2026-03-16 23:49:51 +01:00
commit 2ad076c052
47 changed files with 5294 additions and 0 deletions

153
src/App.tsx Normal file
View 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;