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,158 @@
|
||||
import { useState } from 'react';
|
||||
import axios from '../services/api';
|
||||
|
||||
const DELIMITERS = [
|
||||
{ label: 'Komma (,)', value: ',' },
|
||||
{ label: 'Semikolon (;)', value: ';' },
|
||||
{ label: 'Tab', value: '\\t' },
|
||||
{ label: 'Pipe (|)', value: '|' },
|
||||
];
|
||||
|
||||
function CsvViewerTool() {
|
||||
const [text, setText] = useState('');
|
||||
const [delimiter, setDelimiter] = useState(',');
|
||||
const [result, setResult] = useState(null);
|
||||
const [error, setError] = useState('');
|
||||
const [sortCol, setSortCol] = useState(null);
|
||||
const [sortAsc, setSortAsc] = useState(true);
|
||||
|
||||
const parse = async () => {
|
||||
setError('');
|
||||
setResult(null);
|
||||
setSortCol(null);
|
||||
if (!text.trim()) {
|
||||
setError('Bitte CSV-Inhalt eingeben.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await axios.post('/api/csv/parse', { text, delimiter });
|
||||
setResult(res.data);
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.message || 'Fehler beim Verarbeiten des CSV');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSort = (colIdx) => {
|
||||
if (sortCol === colIdx) {
|
||||
setSortAsc(!sortAsc);
|
||||
} else {
|
||||
setSortCol(colIdx);
|
||||
setSortAsc(true);
|
||||
}
|
||||
};
|
||||
|
||||
const getSortedRows = () => {
|
||||
if (!result) return [];
|
||||
if (sortCol === null) return result.rows;
|
||||
return [...result.rows].sort((a, b) => {
|
||||
const av = a[sortCol] ?? '';
|
||||
const bv = b[sortCol] ?? '';
|
||||
const numA = parseFloat(av);
|
||||
const numB = parseFloat(bv);
|
||||
if (!isNaN(numA) && !isNaN(numB)) {
|
||||
return sortAsc ? numA - numB : numB - numA;
|
||||
}
|
||||
return sortAsc ? av.localeCompare(bv) : bv.localeCompare(av);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
<h2>CSV Viewer</h2>
|
||||
<p style={{ color: 'var(--muted)', marginBottom: '16px', fontSize: '0.95rem' }}>
|
||||
CSV-Daten einfügen und als Tabelle anzeigen.
|
||||
</p>
|
||||
|
||||
<textarea
|
||||
rows={6}
|
||||
value={text}
|
||||
onChange={(e) => { setText(e.target.value); setResult(null); setError(''); }}
|
||||
placeholder="CSV-Inhalt hier einfügen..."
|
||||
style={{ fontFamily: 'monospace', resize: 'vertical' }}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', marginTop: '8px', flexWrap: 'wrap' }}>
|
||||
<select value={delimiter} onChange={(e) => setDelimiter(e.target.value)} style={{ margin: 0 }}>
|
||||
{DELIMITERS.map(({ label, value }) => (
|
||||
<option key={value} value={value}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
<button onClick={parse}>Anzeigen</button>
|
||||
</div>
|
||||
|
||||
{error && <p className="error" style={{ marginTop: '12px' }}>{error}</p>}
|
||||
|
||||
{result && (
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
{result.truncated && (
|
||||
<div style={{
|
||||
padding: '8px 14px', background: 'rgba(245,158,11,0.1)',
|
||||
border: '1px solid rgba(245,158,11,0.3)', borderRadius: '10px',
|
||||
color: '#f59e0b', fontSize: '0.85rem', marginBottom: '10px',
|
||||
}}>
|
||||
Hinweis: Nur die ersten 500 von {result.total_rows} Zeilen werden angezeigt.
|
||||
</div>
|
||||
)}
|
||||
<p style={{ color: 'var(--muted)', fontSize: '0.85rem', marginBottom: '8px' }}>
|
||||
{result.rows.length} Zeilen · {result.headers.length} Spalten
|
||||
{!result.truncated && result.total_rows > 0 && ` · ${result.total_rows} gesamt`}
|
||||
</p>
|
||||
<div style={{ overflowX: 'auto', borderRadius: '12px', border: '1px solid var(--border)' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.88rem' }}>
|
||||
<thead>
|
||||
<tr style={{ background: 'var(--surface-2)' }}>
|
||||
<th style={{
|
||||
padding: '8px 10px', textAlign: 'right', color: 'var(--muted)',
|
||||
fontWeight: 600, borderBottom: '1px solid var(--border)',
|
||||
fontSize: '0.78rem', width: '40px',
|
||||
}}>#</th>
|
||||
{result.headers.map((h, i) => (
|
||||
<th
|
||||
key={i}
|
||||
onClick={() => handleSort(i)}
|
||||
style={{
|
||||
padding: '8px 12px', textAlign: 'left', color: 'var(--text)',
|
||||
fontWeight: 600, borderBottom: '1px solid var(--border)',
|
||||
cursor: 'pointer', userSelect: 'none',
|
||||
background: sortCol === i ? 'rgba(34,211,238,0.06)' : 'transparent',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{h || `Spalte ${i + 1}`}
|
||||
{sortCol === i && (
|
||||
<span style={{ marginLeft: '4px', color: 'var(--accent)' }}>
|
||||
{sortAsc ? '↑' : '↓'}
|
||||
</span>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{getSortedRows().map((row, ri) => (
|
||||
<tr key={ri} style={{ borderBottom: '1px solid var(--border)' }}>
|
||||
<td style={{
|
||||
padding: '6px 10px', textAlign: 'right',
|
||||
color: 'var(--muted)', fontSize: '0.78rem',
|
||||
}}>{ri + 1}</td>
|
||||
{result.headers.map((_, ci) => (
|
||||
<td key={ci} style={{
|
||||
padding: '6px 12px', color: 'var(--text)',
|
||||
fontFamily: 'monospace', maxWidth: '300px',
|
||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{row[ci] ?? ''}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CsvViewerTool;
|
||||
Reference in New Issue
Block a user