german translations and some minor changes to the UI to make it prettier
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -14,3 +14,6 @@ target/
|
|||||||
Cargo.lock
|
Cargo.lock
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
*.pdb
|
*.pdb
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
cache/
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default function ManagePage() {
|
|||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
localStorage.removeItem('adminAuth');
|
localStorage.removeItem('adminAuth');
|
||||||
router.push('/admin');
|
router.push('/');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get current directory contents
|
// Get current directory contents
|
||||||
@@ -110,7 +110,7 @@ export default function ManagePage() {
|
|||||||
|
|
||||||
// Breadcrumbs
|
// Breadcrumbs
|
||||||
const breadcrumbs = [
|
const breadcrumbs = [
|
||||||
{ name: 'Root', path: [] },
|
{ name: '/', path: [] },
|
||||||
...currentPath.map((name, idx) => ({
|
...currentPath.map((name, idx) => ({
|
||||||
name,
|
name,
|
||||||
path: currentPath.slice(0, idx + 1),
|
path: currentPath.slice(0, idx + 1),
|
||||||
@@ -238,39 +238,29 @@ export default function ManagePage() {
|
|||||||
{/* Mobile-friendly header */}
|
{/* 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: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">
|
<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
|
<Link
|
||||||
href="/admin"
|
href="/admin"
|
||||||
className="px-4 py-3 sm:py-2 bg-gray-200 rounded hover:bg-gray-300 transition-colors text-base font-medium"
|
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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={loadContent}
|
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"
|
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">
|
<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" />
|
<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>
|
</svg>
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="px-4 py-3 sm:py-2 bg-red-600 text-white rounded hover:bg-red-700 text-base font-medium"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -281,7 +271,7 @@ export default function ManagePage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPath(currentPath.slice(0, -1))}
|
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"
|
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
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -295,7 +285,7 @@ export default function ManagePage() {
|
|||||||
clipRule="evenodd"
|
clipRule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Back
|
Zurück
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-wrap items-center gap-1 sm:gap-2">
|
<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">
|
<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>
|
<h3 className="text-lg font-bold mb-4">Confirm Delete</h3>
|
||||||
<p className="mb-4 text-sm sm:text-base">
|
<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>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row justify-end gap-3 sm:gap-4">
|
<div className="flex flex-col sm:flex-row justify-end gap-3 sm:gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => setDeleteConfirm({ show: false, item: null })}
|
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"
|
className="px-4 py-3 sm:py-2 bg-gray-200 rounded hover:bg-gray-300 text-base font-medium"
|
||||||
>
|
>
|
||||||
Cancel
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={confirmDelete}
|
onClick={confirmDelete}
|
||||||
className="px-4 py-3 sm:py-2 bg-red-600 text-white rounded hover:bg-red-700 text-base font-medium"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -454,9 +444,9 @@ export default function ManagePage() {
|
|||||||
{deleteAllConfirm.show && (
|
{deleteAllConfirm.show && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
<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">
|
<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">
|
<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 />
|
<br />
|
||||||
<span className="text-red-600">This cannot be undone!</span>
|
<span className="text-red-600">This cannot be undone!</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -465,7 +455,7 @@ export default function ManagePage() {
|
|||||||
onClick={() => setDeleteAllConfirm({ show: false, folder: null })}
|
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"
|
className="px-4 py-3 sm:py-2 bg-gray-200 rounded hover:bg-gray-300 text-base font-medium"
|
||||||
>
|
>
|
||||||
Cancel
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useEffect, useState } from 'react';
|
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 {
|
interface PostStats {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -26,176 +13,139 @@ export default function RustStatusPage() {
|
|||||||
const [stats, setStats] = useState<PostStats[]>([]);
|
const [stats, setStats] = useState<PostStats[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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 () => {
|
const fetchStats = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/admin/posts?rsparseinfo=1');
|
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();
|
const data = await res.json();
|
||||||
setStats(data);
|
setStats(data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e.message || 'Unknown error');
|
setError(e.message || 'Unbekannter Fehler');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStats();
|
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 (
|
return (
|
||||||
<div className="p-8 max-w-6xl mx-auto">
|
<div className="min-h-screen bg-gray-100 p-4 sm:p-6">
|
||||||
<h1 className="text-3xl font-bold mb-8 text-center">Rust Parser Dashboard</h1>
|
<div className="max-w-6xl mx-auto">
|
||||||
<div className="flex justify-end gap-4 mb-4">
|
{/* 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
|
<button
|
||||||
onClick={fetchStats}
|
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>
|
</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>
|
</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>
|
</div>
|
||||||
)}
|
|
||||||
{error && (
|
{/* Rest of your component remains the same */}
|
||||||
<div className="text-red-500 text-center text-lg">{error}</div>
|
|
||||||
)}
|
|
||||||
{!loading && !error && (
|
|
||||||
<>
|
|
||||||
{/* Summary Cards */}
|
{/* Summary Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
<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-6 flex flex-col items-center shadow">
|
<div className="bg-green-100 rounded-lg p-4 flex flex-col items-center shadow">
|
||||||
<span className="text-2xl font-bold text-green-700">{totalHits}</span>
|
<span className="text-xl sm:text-2xl font-bold text-green-700">{totalHits}</span>
|
||||||
<span className="text-gray-700 mt-2">Total Cache Hits</span>
|
<span className="text-sm sm:text-base text-gray-700 mt-1 sm:mt-2 text-center">Cache-Treffer</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-red-100 rounded-lg p-6 flex flex-col items-center shadow">
|
<div className="bg-red-100 rounded-lg p-4 flex flex-col items-center shadow">
|
||||||
<span className="text-2xl font-bold text-red-700">{totalMisses}</span>
|
<span className="text-xl sm:text-2xl font-bold text-red-700">{totalMisses}</span>
|
||||||
<span className="text-gray-700 mt-2">Total Cache Misses</span>
|
<span className="text-sm sm:text-base text-gray-700 mt-1 sm:mt-2 text-center">Cache-Fehlschläge</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-blue-100 rounded-lg p-6 flex flex-col items-center shadow">
|
<div className="bg-blue-100 rounded-lg p-4 flex flex-col items-center shadow">
|
||||||
<span className="text-2xl font-bold text-blue-700">{avgInterpret} ms</span>
|
<span className="text-xl sm:text-2xl font-bold text-blue-700">{avgInterpret} ms</span>
|
||||||
<span className="text-gray-700 mt-2">Avg Interpret Time</span>
|
<span className="text-sm sm:text-base text-gray-700 mt-1 sm:mt-2 text-center">Ø Interpretationszeit</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-purple-100 rounded-lg p-6 flex flex-col items-center shadow">
|
<div className="bg-purple-100 rounded-lg p-4 flex flex-col items-center shadow">
|
||||||
<span className="text-2xl font-bold text-purple-700">{avgCompile} ms</span>
|
<span className="text-xl sm:text-2xl font-bold text-purple-700">{avgCompile} ms</span>
|
||||||
<span className="text-gray-700 mt-2">Avg Compile Time</span>
|
<span className="text-sm sm:text-base text-gray-700 mt-1 sm:mt-2 text-center">Ø Kompilierzeit</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bar Chart */}
|
{/* Table */}
|
||||||
<div className="bg-white rounded-lg shadow p-6 mb-10">
|
<div className="bg-white rounded-lg shadow p-3 sm:p-4 overflow-x-auto">
|
||||||
<Bar data={chartData} options={chartOptions} height={120} />
|
<h2 className="text-base sm:text-lg font-semibold mb-3">Rohdaten</h2>
|
||||||
</div>
|
{loading && <div className="text-center py-6 text-base">Lade Statistiken...</div>}
|
||||||
|
{error && <div className="text-red-500 text-center text-base">{error}</div>}
|
||||||
{/* Raw Data Table */}
|
{!loading && !error && (
|
||||||
<div className="overflow-x-auto">
|
<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>
|
<thead>
|
||||||
<tr className="bg-gray-100">
|
<tr className="bg-gray-100">
|
||||||
<th className="px-4 py-2 text-left">Slug</th>
|
<th className="px-3 py-2 text-left text-sm">Slug</th>
|
||||||
<th className="px-4 py-2 text-right">Cache Hits</th>
|
<th className="px-3 py-2 text-right text-sm">Cache-Treffer</th>
|
||||||
<th className="px-4 py-2 text-right">Cache Misses</th>
|
<th className="px-3 py-2 text-right text-sm">Cache-Fehlschläge</th>
|
||||||
<th className="px-4 py-2 text-right">Last Interpret Time (ms)</th>
|
<th className="px-3 py-2 text-right text-sm">Interpret (ms)</th>
|
||||||
<th className="px-4 py-2 text-right">Last Compile Time (ms)</th>
|
<th className="px-3 py-2 text-right text-sm">Kompilier (ms)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{stats.length === 0 ? (
|
{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 => (
|
stats.map(stat => (
|
||||||
<tr key={stat.slug} className="border-t">
|
<tr key={stat.slug} className="border-t border-gray-200">
|
||||||
<td className="px-4 py-2 font-mono">{stat.slug}</td>
|
<td className="px-3 py-2 font-mono text-sm">{stat.slug}</td>
|
||||||
<td className="px-4 py-2 text-right">{stat.cache_hits}</td>
|
<td className="px-3 py-2 text-right text-sm">{stat.cache_hits}</td>
|
||||||
<td className="px-4 py-2 text-right">{stat.cache_misses}</td>
|
<td className="px-3 py-2 text-right text-sm">{stat.cache_misses}</td>
|
||||||
<td className="px-4 py-2 text-right">{stat.last_interpret_time_ms}</td>
|
<td className="px-3 py-2 text-right text-sm">{stat.last_interpret_time_ms}</td>
|
||||||
<td className="px-4 py-2 text-right">{stat.last_compile_time_ms}</td>
|
<td className="px-3 py-2 text-right text-sm">{stat.last_compile_time_ms}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -812,31 +812,61 @@ export default function AdminPage() {
|
|||||||
<div className="flex flex-col sm:flex-row gap-2">
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
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>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowChangePassword(true)}
|
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>
|
</button>
|
||||||
</div>
|
</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">
|
<div className="flex flex-col sm:flex-row items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleExportTarball}
|
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"
|
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>
|
</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 && (
|
{rememberExportChoice && lastExportChoice && (
|
||||||
<div className="flex items-center gap-1 text-xs text-gray-600 w-full sm:w-auto justify-center sm:justify-start">
|
<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>
|
<span>💾 {lastExportChoice === 'docker' ? 'Docker' : 'Local'}</span>
|
||||||
@@ -960,26 +990,7 @@ export default function AdminPage() {
|
|||||||
Current folder: <span className="font-mono">{currentPath.join('/') || 'root'}</span>
|
Current folder: <span className="font-mono">{currentPath.join('/') || 'root'}</span>
|
||||||
</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="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 */}
|
{/* Drag and Drop Zone */}
|
||||||
<div
|
<div
|
||||||
@@ -991,14 +1002,35 @@ export default function AdminPage() {
|
|||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
>
|
>
|
||||||
<div className="text-gray-600">
|
<div className="text-gray-600">
|
||||||
<p className="text-base sm:text-lg font-medium">Drag and drop Markdown files here</p>
|
<p className="text-base sm:text-lg font-medium">Ziehe Markdown-Dateien hierher</p>
|
||||||
<p className="text-xs sm:text-sm">Files will be uploaded to: {currentPath.join('/') || 'root'}</p>
|
<p className="text-xs sm:text-sm">Dateien werden hochgeladen zu: {currentPath.join('/') || 'root'}</p>
|
||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Create Post Form */}
|
||||||
<div className="bg-white rounded-lg shadow p-4 sm:p-6 mb-6 sm:mb-8">
|
<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">
|
<form onSubmit={editingPost ? handleEditPost : handleCreatePost} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">Title</label>
|
<label className="block text-sm font-medium text-gray-700">Title</label>
|
||||||
@@ -1011,7 +1043,7 @@ export default function AdminPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={newPost.date}
|
value={newPost.date}
|
||||||
@@ -1021,7 +1053,7 @@ export default function AdminPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={newPost.tags}
|
value={newPost.tags}
|
||||||
@@ -1031,7 +1063,7 @@ export default function AdminPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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
|
<textarea
|
||||||
value={newPost.summary}
|
value={newPost.summary}
|
||||||
onChange={(e) => setNewPost({ ...newPost, summary: e.target.value })}
|
onChange={(e) => setNewPost({ ...newPost, summary: e.target.value })}
|
||||||
@@ -1044,7 +1076,7 @@ export default function AdminPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex flex-col sm:flex-row gap-4">
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
<div className="w-full sm:w-1/2">
|
<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
|
<textarea
|
||||||
value={newPost.content}
|
value={newPost.content}
|
||||||
onChange={(e) => setNewPost({ ...newPost, content: e.target.value })}
|
onChange={(e) => setNewPost({ ...newPost, content: e.target.value })}
|
||||||
@@ -1055,7 +1087,7 @@ export default function AdminPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full sm:w-1/2">
|
<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="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 className="prose prose-sm max-w-none" dangerouslySetInnerHTML={{ __html: previewHtml }} />
|
||||||
</div>
|
</div>
|
||||||
@@ -1066,14 +1098,14 @@ export default function AdminPage() {
|
|||||||
type="submit"
|
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"
|
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>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content List */}
|
{/* Content List */}
|
||||||
<div className="bg-white rounded-lg shadow p-4 sm:p-6">
|
<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">
|
<div className="space-y-4">
|
||||||
{/* Folders */}
|
{/* Folders */}
|
||||||
{currentNodes
|
{currentNodes
|
||||||
@@ -1200,7 +1232,7 @@ export default function AdminPage() {
|
|||||||
{showManageContent && (
|
{showManageContent && (
|
||||||
<div className="mt-4 bg-white p-4 sm:p-6 rounded-lg shadow text-center">
|
<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">
|
<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>
|
</p>
|
||||||
{/* Folder navigation breadcrumbs */}
|
{/* Folder navigation breadcrumbs */}
|
||||||
<div className="flex flex-wrap justify-center gap-1 sm:gap-2 mb-4">
|
<div className="flex flex-wrap justify-center gap-1 sm:gap-2 mb-4">
|
||||||
@@ -1208,7 +1240,7 @@ export default function AdminPage() {
|
|||||||
onClick={() => setManagePath([])}
|
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'}`}
|
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>
|
</button>
|
||||||
{managePath.map((name, idx) => (
|
{managePath.map((name, idx) => (
|
||||||
<button
|
<button
|
||||||
@@ -1319,7 +1351,7 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ export default function Home() {
|
|||||||
{/* Last update indicator */}
|
{/* Last update indicator */}
|
||||||
{lastUpdate && (
|
{lastUpdate && (
|
||||||
<div className="text-xs text-gray-500 text-center sm:text-left mb-4">
|
<div className="text-xs text-gray-500 text-center sm:text-left mb-4">
|
||||||
Last updated: {lastUpdate.toLocaleTimeString()}
|
Aktualisiert: {lastUpdate.toLocaleTimeString()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user