german translations and some minor changes to the UI to make it prettier

This commit is contained in:
2025-06-25 21:04:42 +02:00
parent e4c6a7e0a8
commit 2a0a0c9f38
6 changed files with 209 additions and 308 deletions

3
.gitignore vendored
View File

@@ -14,3 +14,6 @@ target/
Cargo.lock
**/*.rs.bk
*.pdb
# Cache
cache/

View File

@@ -87,7 +87,7 @@ export default function ManagePage() {
const handleLogout = () => {
setIsAuthenticated(false);
localStorage.removeItem('adminAuth');
router.push('/admin');
router.push('/');
};
// Get current directory contents
@@ -110,7 +110,7 @@ export default function ManagePage() {
// Breadcrumbs
const breadcrumbs = [
{ name: 'Root', path: [] },
{ name: '/', path: [] },
...currentPath.map((name, idx) => ({
name,
path: currentPath.slice(0, idx + 1),
@@ -238,39 +238,29 @@ export default function ManagePage() {
{/* Mobile-friendly header */}
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-6 sm:mb-8 space-y-4 sm:space-y-0">
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4">
<h1 className="text-2xl sm:text-3xl font-bold">Manage Content</h1>
<h1 className="text-2xl sm:text-3xl font-bold">Inhaltsverwaltung</h1>
<Link
href="/admin"
className="px-4 py-3 sm:py-2 bg-gray-200 rounded hover:bg-gray-300 transition-colors text-base font-medium"
>
Back to Admin
Zum Admin-Panel
</Link>
</div>
<div className="flex gap-2">
<button
onClick={loadContent}
className="px-4 py-3 sm:py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-base font-medium"
title="Refresh content"
title="Inhalt aktualisieren"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
<Link
href="/admin/manage/rust-status"
className="px-4 py-3 sm:py-2 bg-teal-600 text-white rounded hover:bg-teal-700 transition-colors text-base font-medium flex items-center"
title="Rust Parser Status"
>
<svg className="h-5 w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M12 20a8 8 0 100-16 8 8 0 000 16z" />
</svg>
Rust Parser Status
</Link>
<button
onClick={handleLogout}
className="px-4 py-3 sm:py-2 bg-red-600 text-white rounded hover:bg-red-700 text-base font-medium"
>
Logout
Abmelden
</button>
</div>
</div>
@@ -281,7 +271,7 @@ export default function ManagePage() {
<button
onClick={() => setCurrentPath(currentPath.slice(0, -1))}
className="flex items-center gap-2 px-3 sm:px-4 py-2 bg-gray-200 rounded hover:bg-gray-300 transition-colors text-sm sm:text-base"
title="Go back one level"
title="Einen Ordner zurück"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -295,7 +285,7 @@ export default function ManagePage() {
clipRule="evenodd"
/>
</svg>
Back
Zurück
</button>
)}
<div className="flex flex-wrap items-center gap-1 sm:gap-2">
@@ -430,20 +420,20 @@ export default function ManagePage() {
<div className="bg-white p-4 sm:p-6 rounded-lg shadow-xl max-w-sm w-full">
<h3 className="text-lg font-bold mb-4">Confirm Delete</h3>
<p className="mb-4 text-sm sm:text-base">
Are you sure you want to delete {deleteConfirm.item?.type === 'folder' ? 'folder' : 'post'} "{deleteConfirm.item?.type === 'folder' ? deleteConfirm.item.name : deleteConfirm.item?.title}"?
Sind Sie sicher, dass Sie {deleteConfirm.item?.type === 'folder' ? 'Ordner' : 'Beitrag'} "{deleteConfirm.item?.type === 'folder' ? deleteConfirm.item.name : deleteConfirm.item?.title}" löschen möchten?
</p>
<div className="flex flex-col sm:flex-row justify-end gap-3 sm:gap-4">
<button
onClick={() => setDeleteConfirm({ show: false, item: null })}
className="px-4 py-3 sm:py-2 bg-gray-200 rounded hover:bg-gray-300 text-base font-medium"
>
Cancel
Abbrechen
</button>
<button
onClick={confirmDelete}
className="px-4 py-3 sm:py-2 bg-red-600 text-white rounded hover:bg-red-700 text-base font-medium"
>
Delete
Löschen
</button>
</div>
</div>
@@ -454,9 +444,9 @@ export default function ManagePage() {
{deleteAllConfirm.show && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white p-4 sm:p-6 rounded-lg shadow-xl max-w-sm w-full">
<h3 className="text-lg font-bold mb-4">Delete Full Folder</h3>
<h3 className="text-lg font-bold mb-4">Lösche Ordner</h3>
<p className="mb-4 text-sm sm:text-base">
Are you sure you want to <b>delete the entire folder and all its contents</b>?
Sind Sie sicher, dass Sie <b>den gesamten Ordner und alle Inhalte löschen</b> möchten?
<br />
<span className="text-red-600">This cannot be undone!</span>
</p>
@@ -465,7 +455,7 @@ export default function ManagePage() {
onClick={() => setDeleteAllConfirm({ show: false, folder: null })}
className="px-4 py-3 sm:py-2 bg-gray-200 rounded hover:bg-gray-300 text-base font-medium"
>
Cancel
Abbrechen
</button>
<button
onClick={async () => {
@@ -486,7 +476,7 @@ export default function ManagePage() {
}}
className="px-4 py-3 sm:py-2 bg-red-600 text-white rounded hover:bg-red-700 text-base font-medium"
>
Delete All
Löschen
</button>
</div>
</div>

View File

@@ -1,74 +0,0 @@
import React, { useEffect, useState } from 'react';
interface PostStats {
slug: string;
cache_hits: number;
cache_misses: number;
last_interpret_time_ms: number;
last_compile_time_ms: number;
}
export default function RustStatusPage() {
const [stats, setStats] = useState<PostStats[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchStats = async () => {
setLoading(true);
setError(null);
try {
const res = await fetch('/api/admin/posts?rsparseinfo=1');
if (!res.ok) throw new Error('Failed to fetch stats');
const data = await res.json();
setStats(data);
} catch (e: any) {
setError(e.message || 'Unknown error');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchStats();
const interval = setInterval(fetchStats, 5000);
return () => clearInterval(interval);
}, []);
return (
<div className="p-8 max-w-4xl mx-auto">
<h1 className="text-2xl font-bold mb-6">Rust Parser Status</h1>
{loading && <div>Loading...</div>}
{error && <div className="text-red-500">{error}</div>}
{!loading && !error && (
<div className="overflow-x-auto">
<table className="min-w-full border border-gray-300 bg-white shadow-md rounded">
<thead>
<tr className="bg-gray-100">
<th className="px-4 py-2 text-left">Slug</th>
<th className="px-4 py-2 text-right">Cache Hits</th>
<th className="px-4 py-2 text-right">Cache Misses</th>
<th className="px-4 py-2 text-right">Last Interpret Time (ms)</th>
<th className="px-4 py-2 text-right">Last Compile Time (ms)</th>
</tr>
</thead>
<tbody>
{stats.length === 0 ? (
<tr><td colSpan={5} className="text-center py-4">No stats available.</td></tr>
) : (
stats.map(stat => (
<tr key={stat.slug} className="border-t">
<td className="px-4 py-2 font-mono">{stat.slug}</td>
<td className="px-4 py-2 text-right">{stat.cache_hits}</td>
<td className="px-4 py-2 text-right">{stat.cache_misses}</td>
<td className="px-4 py-2 text-right">{stat.last_interpret_time_ms}</td>
<td className="px-4 py-2 text-right">{stat.last_compile_time_ms}</td>
</tr>
))
)}
</tbody>
</table>
</div>
)}
</div>
);
}

View File

@@ -1,18 +1,5 @@
'use client';
import React, { useEffect, useState } from 'react';
import { Bar } from 'react-chartjs-2';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
ChartOptions,
} from 'chart.js';
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
interface PostStats {
slug: string;
@@ -26,176 +13,139 @@ export default function RustStatusPage() {
const [stats, setStats] = useState<PostStats[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [autoRefresh, setAutoRefresh] = useState(false);
const autoRefreshRef = React.useRef<NodeJS.Timeout | null>(null);
// Summary calculations
const totalHits = stats.reduce((sum, s) => sum + s.cache_hits, 0);
const totalMisses = stats.reduce((sum, s) => sum + s.cache_misses, 0);
const avgInterpret = stats.length ? (stats.reduce((sum, s) => sum + s.last_interpret_time_ms, 0) / stats.length).toFixed(1) : 0;
const avgCompile = stats.length ? (stats.reduce((sum, s) => sum + s.last_compile_time_ms, 0) / stats.length).toFixed(1) : 0;
const fetchStats = async () => {
setLoading(true);
setError(null);
try {
const res = await fetch('/api/admin/posts?rsparseinfo=1');
if (!res.ok) throw new Error('Failed to fetch stats');
if (!res.ok) throw new Error('Fehler beim Laden der Statistiken');
const data = await res.json();
setStats(data);
} catch (e: any) {
setError(e.message || 'Unknown error');
setError(e.message || 'Unbekannter Fehler');
} finally {
setLoading(false);
}
};
React.useEffect(() => {
useEffect(() => {
fetchStats();
// Listen for post changes via BroadcastChannel
let bc: BroadcastChannel | null = null;
if (typeof window !== 'undefined' && 'BroadcastChannel' in window) {
bc = new BroadcastChannel('posts-changed');
bc.onmessage = (event) => {
if (event.data === 'changed') {
fetchStats();
}
};
}
return () => {
if (bc) bc.close();
if (autoRefreshRef.current) clearInterval(autoRefreshRef.current);
};
}, []);
// Handle auto-refresh toggle
React.useEffect(() => {
if (autoRefresh) {
autoRefreshRef.current = setInterval(fetchStats, 2000);
} else if (autoRefreshRef.current) {
clearInterval(autoRefreshRef.current);
autoRefreshRef.current = null;
}
return () => {
if (autoRefreshRef.current) clearInterval(autoRefreshRef.current);
};
}, [autoRefresh]);
// Dashboard summary calculations
const totalHits = stats.reduce((sum, s) => sum + s.cache_hits, 0);
const totalMisses = stats.reduce((sum, s) => sum + s.cache_misses, 0);
const avgInterpret = stats.length ? (stats.reduce((sum, s) => sum + s.last_interpret_time_ms, 0) / stats.length).toFixed(1) : 0;
const avgCompile = stats.length ? (stats.reduce((sum, s) => sum + s.last_compile_time_ms, 0) / stats.length).toFixed(1) : 0;
// Chart data
const chartData = {
labels: stats.map(s => s.slug),
datasets: [
{
label: 'Cache Hits',
data: stats.map(s => s.cache_hits),
backgroundColor: 'rgba(34,197,94,0.7)',
},
{
label: 'Cache Misses',
data: stats.map(s => s.cache_misses),
backgroundColor: 'rgba(239,68,68,0.7)',
},
],
};
const chartOptions: ChartOptions<'bar'> = {
responsive: true,
plugins: {
legend: { position: 'top' },
title: { display: true, text: 'Cache Hits & Misses per Post' },
},
scales: {
x: { stacked: true },
y: { stacked: true, beginAtZero: true },
},
};
return (
<div className="p-8 max-w-6xl mx-auto">
<h1 className="text-3xl font-bold mb-8 text-center">Rust Parser Dashboard</h1>
<div className="flex justify-end gap-4 mb-4">
<div className="min-h-screen bg-gray-100 p-4 sm:p-6">
<div className="max-w-6xl mx-auto">
{/* Header with title and action buttons */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
<div className="flex items-center gap-3">
<div className="bg-white rounded-lg shadow p-2 flex items-center justify-center">
<img
className="w-10 h-10 sm:w-12 sm:h-12"
src="https://upload.wikimedia.org/wikipedia/commons/d/d5/Rust_programming_language_black_logo.svg"
alt="Rust Logo"
/>
</div>
<h1 className="text-xl sm:text-2xl font-bold">Rust-Parser Statistiken</h1>
</div>
<div className="flex items-center gap-2 w-full sm:w-auto justify-end">
{/* Back to Admin button */}
<a
href="/admin"
className="p-2 sm:px-4 sm:py-2 bg-gray-200 hover:bg-gray-300 rounded-lg shadow flex items-center gap-1 transition-colors"
title="Zurück zur Admin-Panel"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
<span className="hidden sm:inline">Zurück zur Admin-Panel</span>
</a>
{/* Refresh button */}
<button
onClick={fetchStats}
className="px-4 py-2 bg-blue-600 text-white rounded shadow hover:bg-blue-700"
className="p-2 sm:px-4 sm:py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg shadow flex items-center gap-1 transition-colors"
title="Aktualisieren"
disabled={loading}
>
Refresh
<svg
className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span className="hidden sm:inline">Aktualisieren</span>
</button>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={autoRefresh}
onChange={e => setAutoRefresh(e.target.checked)}
className="form-checkbox"
/>
<span className="text-sm">Auto-refresh every 2s</span>
</label>
</div>
{loading && (
<div className="flex flex-col items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900 mb-4"></div>
<div className="text-lg">Loading stats...</div>
</div>
)}
{error && (
<div className="text-red-500 text-center text-lg">{error}</div>
)}
{!loading && !error && (
<>
{/* Rest of your component remains the same */}
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-green-100 rounded-lg p-6 flex flex-col items-center shadow">
<span className="text-2xl font-bold text-green-700">{totalHits}</span>
<span className="text-gray-700 mt-2">Total Cache Hits</span>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4 mb-6">
<div className="bg-green-100 rounded-lg p-4 flex flex-col items-center shadow">
<span className="text-xl sm:text-2xl font-bold text-green-700">{totalHits}</span>
<span className="text-sm sm:text-base text-gray-700 mt-1 sm:mt-2 text-center">Cache-Treffer</span>
</div>
<div className="bg-red-100 rounded-lg p-6 flex flex-col items-center shadow">
<span className="text-2xl font-bold text-red-700">{totalMisses}</span>
<span className="text-gray-700 mt-2">Total Cache Misses</span>
<div className="bg-red-100 rounded-lg p-4 flex flex-col items-center shadow">
<span className="text-xl sm:text-2xl font-bold text-red-700">{totalMisses}</span>
<span className="text-sm sm:text-base text-gray-700 mt-1 sm:mt-2 text-center">Cache-Fehlschläge</span>
</div>
<div className="bg-blue-100 rounded-lg p-6 flex flex-col items-center shadow">
<span className="text-2xl font-bold text-blue-700">{avgInterpret} ms</span>
<span className="text-gray-700 mt-2">Avg Interpret Time</span>
<div className="bg-blue-100 rounded-lg p-4 flex flex-col items-center shadow">
<span className="text-xl sm:text-2xl font-bold text-blue-700">{avgInterpret} ms</span>
<span className="text-sm sm:text-base text-gray-700 mt-1 sm:mt-2 text-center">Ø Interpretationszeit</span>
</div>
<div className="bg-purple-100 rounded-lg p-6 flex flex-col items-center shadow">
<span className="text-2xl font-bold text-purple-700">{avgCompile} ms</span>
<span className="text-gray-700 mt-2">Avg Compile Time</span>
<div className="bg-purple-100 rounded-lg p-4 flex flex-col items-center shadow">
<span className="text-xl sm:text-2xl font-bold text-purple-700">{avgCompile} ms</span>
<span className="text-sm sm:text-base text-gray-700 mt-1 sm:mt-2 text-center">Ø Kompilierzeit</span>
</div>
</div>
{/* Bar Chart */}
<div className="bg-white rounded-lg shadow p-6 mb-10">
<Bar data={chartData} options={chartOptions} height={120} />
</div>
{/* Raw Data Table */}
{/* Table */}
<div className="bg-white rounded-lg shadow p-3 sm:p-4 overflow-x-auto">
<h2 className="text-base sm:text-lg font-semibold mb-3">Rohdaten</h2>
{loading && <div className="text-center py-6 text-base">Lade Statistiken...</div>}
{error && <div className="text-red-500 text-center text-base">{error}</div>}
{!loading && !error && (
<div className="overflow-x-auto">
<table className="min-w-full border border-gray-300 bg-white shadow-md rounded">
<table className="min-w-full border border-gray-200 bg-white rounded">
<thead>
<tr className="bg-gray-100">
<th className="px-4 py-2 text-left">Slug</th>
<th className="px-4 py-2 text-right">Cache Hits</th>
<th className="px-4 py-2 text-right">Cache Misses</th>
<th className="px-4 py-2 text-right">Last Interpret Time (ms)</th>
<th className="px-4 py-2 text-right">Last Compile Time (ms)</th>
<th className="px-3 py-2 text-left text-sm">Slug</th>
<th className="px-3 py-2 text-right text-sm">Cache-Treffer</th>
<th className="px-3 py-2 text-right text-sm">Cache-Fehlschläge</th>
<th className="px-3 py-2 text-right text-sm">Interpret (ms)</th>
<th className="px-3 py-2 text-right text-sm">Kompilier (ms)</th>
</tr>
</thead>
<tbody>
{stats.length === 0 ? (
<tr><td colSpan={5} className="text-center py-4">No stats available.</td></tr>
<tr><td colSpan={5} className="text-center py-3 text-sm">Keine Statistiken verfügbar.</td></tr>
) : (
stats.map(stat => (
<tr key={stat.slug} className="border-t">
<td className="px-4 py-2 font-mono">{stat.slug}</td>
<td className="px-4 py-2 text-right">{stat.cache_hits}</td>
<td className="px-4 py-2 text-right">{stat.cache_misses}</td>
<td className="px-4 py-2 text-right">{stat.last_interpret_time_ms}</td>
<td className="px-4 py-2 text-right">{stat.last_compile_time_ms}</td>
<tr key={stat.slug} className="border-t border-gray-200">
<td className="px-3 py-2 font-mono text-sm">{stat.slug}</td>
<td className="px-3 py-2 text-right text-sm">{stat.cache_hits}</td>
<td className="px-3 py-2 text-right text-sm">{stat.cache_misses}</td>
<td className="px-3 py-2 text-right text-sm">{stat.last_interpret_time_ms}</td>
<td className="px-3 py-2 text-right text-sm">{stat.last_compile_time_ms}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</>
)}
</div>
</div>
</div>
);
}

View File

@@ -812,31 +812,61 @@ export default function AdminPage() {
<div className="flex flex-col sm:flex-row gap-2">
<button
onClick={handleLogout}
className="w-full sm:w-auto px-4 py-3 sm:py-2 bg-red-600 text-white rounded hover:bg-red-700 text-sm sm:text-base font-medium"
className="w-full sm:w-auto px-4 py-3 sm:py-2 bg-gradient-to-r from-red-600 to-pink-500 text-white rounded-xl shadow-lg flex items-center justify-center gap-2 text-sm sm:text-base font-semibold hover:from-red-700 hover:to-pink-600 transition-all focus:outline-none focus:ring-2 focus:ring-red-400"
title="Logout"
>
Logout
<svg className="h-5 w-5 sm:h-6 sm:w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
<span className="flex flex-col items-start">
<span>Abmelden</span>
<span className="text-xs font-normal text-red-100">Ausloggen</span>
</span>
</button>
<button
onClick={() => setShowChangePassword(true)}
className="w-full sm:w-auto px-4 py-3 sm:py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm sm:text-base font-medium"
className="w-full sm:w-auto px-4 py-3 sm:py-2 bg-gradient-to-r from-blue-600 to-cyan-500 text-white rounded-xl shadow-lg flex items-center justify-center gap-2 text-sm sm:text-base font-semibold hover:from-blue-700 hover:to-cyan-600 transition-all focus:outline-none focus:ring-2 focus:ring-blue-400"
title="Passwort ändern"
>
Passwort ändern
<svg className="h-5 w-5 sm:h-6 sm:w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 11c1.104 0 2-.896 2-2s-.896-2-2-2-2 .896-2 2 .896 2 2 2zm6 2v5a2 2 0 01-2 2H8a2 2 0 01-2-2v-5m12 0V9a6 6 0 10-12 0v4m12 0H6" />
</svg>
<span className="flex flex-col items-start">
<span>Passwort ändern</span>
<span className="text-xs font-normal text-blue-100">Passwort ändern</span>
</span>
</button>
</div>
{/* Docker warning above export button */}
{isDocker && (
<div className="px-4 py-2 bg-yellow-200 text-yellow-900 rounded border border-yellow-400 font-semibold text-xs sm:text-sm text-center">
<span className="font-bold">Warning:</span> Docker is in use. Exporting will export the entire <span className="font-mono">/app</span> root directory (including all files and folders in the container's root).
</div>
)}
<div className="flex flex-col sm:flex-row items-center gap-2">
<button
onClick={handleExportTarball}
className="w-full sm:w-auto px-4 py-3 sm:py-2 bg-green-600 text-white rounded hover:bg-green-700 text-sm sm:text-base font-medium whitespace-nowrap"
className="w-full sm:w-auto px-4 py-3 sm:py-2 bg-gradient-to-r from-green-600 to-emerald-500 text-white rounded-xl shadow-lg flex items-center justify-center gap-2 text-sm sm:text-base font-semibold hover:from-green-700 hover:to-emerald-600 transition-all focus:outline-none focus:ring-2 focus:ring-green-400"
title="Export Docker Posts"
>
Export Posts
<svg className="h-5 w-5 sm:h-6 sm:w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<rect x="4" y="4" width="16" height="16" rx="3" stroke="currentColor" strokeWidth="2" fill="none" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 12h8M12 8v8" />
</svg>
<span className="flex flex-col items-start">
<span>Exportieren</span>
<span className="text-xs font-normal text-green-100">Alle exportieren</span>
</span>
</button>
<a
href="/admin/manage/rust-status"
className="w-full sm:w-auto px-4 py-3 sm:py-2 bg-gradient-to-r from-teal-500 to-blue-500 text-white rounded-xl shadow-lg flex items-center justify-center gap-2 text-sm sm:text-base font-semibold hover:from-teal-600 hover:to-blue-600 transition-all focus:outline-none focus:ring-2 focus:ring-teal-400"
title="View Rust Parser Dashboard"
style={{ minWidth: '160px' }}
>
<svg className="h-5 w-5 sm:h-6 sm:w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" fill="none" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01" />
</svg>
<span className="flex flex-col items-start">
<span>Rust-Parser</span>
<span className="text-xs font-normal text-teal-100">Statistiken</span>
</span>
</a>
{rememberExportChoice && lastExportChoice && (
<div className="flex items-center gap-1 text-xs text-gray-600 w-full sm:w-auto justify-center sm:justify-start">
<span>💾 {lastExportChoice === 'docker' ? 'Docker' : 'Local'}</span>
@@ -960,26 +990,7 @@ export default function AdminPage() {
Current folder: <span className="font-mono">{currentPath.join('/') || 'root'}</span>
</div>
{/* Create Folder Form */}
<div className="bg-white rounded-lg shadow p-4 sm:p-6 mb-6 sm:mb-8">
<h2 className="text-xl sm:text-2xl font-bold mb-4">Create New Folder</h2>
<form onSubmit={handleCreateFolder} className="flex flex-col sm:flex-row gap-3 sm:gap-4">
<input
type="text"
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
placeholder="Folder name"
className="flex-1 rounded-md border border-gray-300 px-3 py-2 text-base"
required
/>
<button
type="submit"
className="px-4 py-3 sm:py-2 bg-green-600 text-white rounded hover:bg-green-700 text-base font-medium"
>
Create Folder
</button>
</form>
</div>
{/* Drag and Drop Zone */}
<div
@@ -991,14 +1002,35 @@ export default function AdminPage() {
onDrop={handleDrop}
>
<div className="text-gray-600">
<p className="text-base sm:text-lg font-medium">Drag and drop Markdown files here</p>
<p className="text-xs sm:text-sm">Files will be uploaded to: {currentPath.join('/') || 'root'}</p>
<p className="text-base sm:text-lg font-medium">Ziehe Markdown-Dateien hierher</p>
<p className="text-xs sm:text-sm">Dateien werden hochgeladen zu: {currentPath.join('/') || 'root'}</p>
</div>
</div>
{/* Create Folder Form */}
<div className="bg-white rounded-lg shadow p-4 sm:p-6 mb-6 sm:mb-8">
<h2 className="text-xl sm:text-2xl font-bold mb-4">Create New Folder</h2>
<form onSubmit={handleCreateFolder} className="flex flex-col sm:flex-row gap-3 sm:gap-4">
<input
type="text"
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
placeholder="Ordnername"
className="flex-1 rounded-md border border-gray-300 px-3 py-2 text-base"
required
/>
<button
type="submit"
className="px-4 py-3 sm:py-2 bg-green-600 text-white rounded hover:bg-green-700 text-base font-medium"
>
Ordner erstellen
</button>
</form>
</div>
{/* Create Post Form */}
<div className="bg-white rounded-lg shadow p-4 sm:p-6 mb-6 sm:mb-8">
<h2 className="text-xl sm:text-2xl font-bold mb-4">Create New Post</h2>
<h2 className="text-xl sm:text-2xl font-bold mb-4">Erstelle neuen Beitrag</h2>
<form onSubmit={editingPost ? handleEditPost : handleCreatePost} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">Title</label>
@@ -1011,7 +1043,7 @@ export default function AdminPage() {
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Date</label>
<label className="block text-sm font-medium text-gray-700">Datum</label>
<input
type="date"
value={newPost.date}
@@ -1021,7 +1053,7 @@ export default function AdminPage() {
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Tags (comma-separated)</label>
<label className="block text-sm font-medium text-gray-700">Tags (komma-getrennt)</label>
<input
type="text"
value={newPost.tags}
@@ -1031,7 +1063,7 @@ export default function AdminPage() {
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Summary</label>
<label className="block text-sm font-medium text-gray-700">Zusammenfassung</label>
<textarea
value={newPost.summary}
onChange={(e) => setNewPost({ ...newPost, summary: e.target.value })}
@@ -1044,7 +1076,7 @@ export default function AdminPage() {
<div className="space-y-4">
<div className="flex flex-col sm:flex-row gap-4">
<div className="w-full sm:w-1/2">
<label className="block text-sm font-medium text-gray-700 mb-2">Content (Markdown)</label>
<label className="block text-sm font-medium text-gray-700 mb-2">Inhalt (Markdown)</label>
<textarea
value={newPost.content}
onChange={(e) => setNewPost({ ...newPost, content: e.target.value })}
@@ -1055,7 +1087,7 @@ export default function AdminPage() {
/>
</div>
<div className="w-full sm:w-1/2">
<label className="block text-sm font-medium text-gray-700 mb-2">Live Preview</label>
<label className="block text-sm font-medium text-gray-700 mb-2">Vorschau</label>
<div className="p-3 sm:p-4 border rounded bg-gray-50 overflow-auto" style={{ height: '240px' }}>
<div className="prose prose-sm max-w-none" dangerouslySetInnerHTML={{ __html: previewHtml }} />
</div>
@@ -1066,14 +1098,14 @@ export default function AdminPage() {
type="submit"
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-base font-medium text-white bg-blue-600 hover:bg-blue-700"
>
{editingPost ? 'Save Changes' : 'Create Post'}
{editingPost ? 'Speichern' : 'Beitrag erstellen'}
</button>
</form>
</div>
{/* Content List */}
<div className="bg-white rounded-lg shadow p-4 sm:p-6">
<h2 className="text-xl sm:text-2xl font-bold mb-4">Content</h2>
<h2 className="text-xl sm:text-2xl font-bold mb-4">Inhalt:</h2>
<div className="space-y-4">
{/* Folders */}
{currentNodes
@@ -1200,7 +1232,7 @@ export default function AdminPage() {
{showManageContent && (
<div className="mt-4 bg-white p-4 sm:p-6 rounded-lg shadow text-center">
<p className="text-gray-600 mb-2 text-sm sm:text-base">
Delete posts and folders, manage your content structure
Lösche Beiträge und Ordner, verwalte deine Inhaltsstruktur
</p>
{/* Folder navigation breadcrumbs */}
<div className="flex flex-wrap justify-center gap-1 sm:gap-2 mb-4">
@@ -1208,7 +1240,7 @@ export default function AdminPage() {
onClick={() => setManagePath([])}
className={`px-2 py-1 rounded text-sm sm:text-base ${managePath.length === 0 ? 'bg-blue-100 text-blue-800' : 'hover:bg-gray-200'}`}
>
Root
/
</button>
{managePath.map((name, idx) => (
<button
@@ -1319,7 +1351,7 @@ export default function AdminPage() {
</div>
))}
</div>
<a href="/admin/manage" className="block mt-6 text-blue-600 hover:underline text-sm sm:text-base">Go to Content Manager</a>
<a href="/admin/manage" className="block mt-6 text-blue-600 hover:underline text-sm sm:text-base">Zur Inhaltsverwaltung</a>
</div>
)}
</div>

View File

@@ -214,7 +214,7 @@ export default function Home() {
{/* Last update indicator */}
{lastUpdate && (
<div className="text-xs text-gray-500 text-center sm:text-left mb-4">
Last updated: {lastUpdate.toLocaleTimeString()}
Aktualisiert: {lastUpdate.toLocaleTimeString()}
</div>
)}