Add 8 new tools: Hash Verifier, URL Tool, String Utils, Cron Explainer, IP Calc, Lorem Ipsum, CSV Viewer, Notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import axios from '../services/api';
|
||||
|
||||
const LANGUAGES = [
|
||||
{ value: 'text', label: 'Text' },
|
||||
{ value: 'python', label: 'Python' },
|
||||
{ value: 'javascript', label: 'JavaScript' },
|
||||
{ value: 'sql', label: 'SQL' },
|
||||
{ value: 'bash', label: 'Bash' },
|
||||
{ value: 'json', label: 'JSON' },
|
||||
];
|
||||
|
||||
function NotesTool() {
|
||||
const [notes, setNotes] = useState([]);
|
||||
const [selected, setSelected] = useState(null);
|
||||
const [title, setTitle] = useState('Neue Notiz');
|
||||
const [content, setContent] = useState('');
|
||||
const [language, setLanguage] = useState('text');
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const debounceRef = useRef(null);
|
||||
const isNew = selected === null;
|
||||
|
||||
const loadNotes = useCallback(async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/notes');
|
||||
setNotes(res.data);
|
||||
} catch {
|
||||
setError('Fehler beim Laden der Notizen');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { loadNotes(); }, [loadNotes]);
|
||||
|
||||
const selectNote = (note) => {
|
||||
setSelected(note.id);
|
||||
setTitle(note.title);
|
||||
setContent(note.content || '');
|
||||
setLanguage(note.language || 'text');
|
||||
setSaved(false);
|
||||
setConfirmDelete(false);
|
||||
setError('');
|
||||
};
|
||||
|
||||
const newNote = () => {
|
||||
setSelected(null);
|
||||
setTitle('Neue Notiz');
|
||||
setContent('');
|
||||
setLanguage('text');
|
||||
setSaved(false);
|
||||
setConfirmDelete(false);
|
||||
setError('');
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
setError('');
|
||||
try {
|
||||
if (isNew) {
|
||||
const res = await axios.post('/api/notes', { title, content, language });
|
||||
setSelected(res.data.id);
|
||||
await loadNotes();
|
||||
} else {
|
||||
await axios.put(`/api/notes/${selected}`, { title, content, language });
|
||||
await loadNotes();
|
||||
}
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.message || 'Fehler beim Speichern');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteNote = async () => {
|
||||
if (!confirmDelete) { setConfirmDelete(true); return; }
|
||||
try {
|
||||
await axios.delete(`/api/notes/${selected}`);
|
||||
setNotes(prev => prev.filter(n => n.id !== selected));
|
||||
newNote();
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.message || 'Fehler beim Löschen');
|
||||
}
|
||||
};
|
||||
|
||||
// Debounced auto-save for existing notes
|
||||
useEffect(() => {
|
||||
if (isNew) return;
|
||||
clearTimeout(debounceRef.current);
|
||||
debounceRef.current = setTimeout(async () => {
|
||||
try {
|
||||
await axios.put(`/api/notes/${selected}`, { title, content, language });
|
||||
await loadNotes();
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 1500);
|
||||
} catch { /* silent */ }
|
||||
}, 1000);
|
||||
return () => clearTimeout(debounceRef.current);
|
||||
}, [title, content, language]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const formatDate = (iso) => {
|
||||
if (!iso) return '';
|
||||
return new Date(iso).toLocaleString('de-DE', {
|
||||
day: '2-digit', month: '2-digit', year: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="main-content" style={{ maxWidth: '1100px' }}>
|
||||
<h2>Notizen & Snippets</h2>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '260px 1fr', gap: '16px', alignItems: 'start' }}>
|
||||
{/* Sidebar */}
|
||||
<div style={{
|
||||
background: 'var(--surface-2)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{ padding: '10px', borderBottom: '1px solid var(--border)' }}>
|
||||
<button onClick={newNote} style={{ width: '100%', margin: 0, padding: '8px', fontSize: '0.85rem' }}>
|
||||
+ Neue Notiz
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ maxHeight: '500px', overflowY: 'auto' }}>
|
||||
{notes.length === 0 ? (
|
||||
<p style={{ color: 'var(--muted)', padding: '16px', fontSize: '0.85rem', textAlign: 'center' }}>
|
||||
Keine Notizen vorhanden
|
||||
</p>
|
||||
) : notes.map((note) => (
|
||||
<div
|
||||
key={note.id}
|
||||
onClick={() => selectNote(note)}
|
||||
style={{
|
||||
padding: '10px 14px',
|
||||
cursor: 'pointer',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
background: selected === note.id ? 'rgba(34,211,238,0.08)' : 'transparent',
|
||||
borderLeft: selected === note.id ? '3px solid var(--accent)' : '3px solid transparent',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
fontWeight: 600, color: 'var(--text)', fontSize: '0.9rem',
|
||||
marginBottom: '2px', overflow: 'hidden',
|
||||
textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{note.title}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>
|
||||
{formatDate(note.updated_at)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor */}
|
||||
<div style={{
|
||||
background: 'var(--surface-2)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
}}>
|
||||
<div style={{ display: 'flex', gap: '8px', marginBottom: '10px', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => { setTitle(e.target.value); setSaved(false); }}
|
||||
placeholder="Titel"
|
||||
style={{ flex: 1, margin: 0, minWidth: '150px' }}
|
||||
/>
|
||||
<select
|
||||
value={language}
|
||||
onChange={(e) => { setLanguage(e.target.value); setSaved(false); }}
|
||||
style={{ margin: 0, width: 'auto' }}
|
||||
>
|
||||
{LANGUAGES.map(({ value, label }) => (
|
||||
<option key={value} value={value}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
rows={16}
|
||||
value={content}
|
||||
onChange={(e) => { setContent(e.target.value); setSaved(false); }}
|
||||
placeholder="Inhalt..."
|
||||
style={{ fontFamily: 'monospace', resize: 'vertical', fontSize: '0.88rem', margin: 0 }}
|
||||
/>
|
||||
|
||||
{error && <p className="error" style={{ marginTop: '8px', marginBottom: 0 }}>{error}</p>}
|
||||
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '10px', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<button onClick={save} style={{ margin: 0 }}>
|
||||
{isNew ? 'Erstellen' : 'Speichern'}
|
||||
</button>
|
||||
{!isNew && (
|
||||
<button
|
||||
className="ghost"
|
||||
onClick={deleteNote}
|
||||
style={{
|
||||
margin: 0,
|
||||
color: confirmDelete ? '#ef4444' : 'var(--text)',
|
||||
borderColor: confirmDelete ? '#ef4444' : 'var(--border)',
|
||||
}}
|
||||
>
|
||||
{confirmDelete ? 'Wirklich löschen?' : 'Löschen'}
|
||||
</button>
|
||||
)}
|
||||
{confirmDelete && (
|
||||
<button className="ghost" onClick={() => setConfirmDelete(false)} style={{ margin: 0 }}>
|
||||
Abbrechen
|
||||
</button>
|
||||
)}
|
||||
{saved && (
|
||||
<span style={{ color: '#22c55e', fontSize: '0.85rem', fontWeight: 600 }}>✓ Gespeichert</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NotesTool;
|
||||
Reference in New Issue
Block a user