fix Analyse
This commit is contained in:
181
frontend/src/pages/Analytics.jsx
Normal file
181
frontend/src/pages/Analytics.jsx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import {
|
||||||
|
fetchAnalyticsOverview, fetchAnalyticsPageviews,
|
||||||
|
fetchAnalyticsUsers, fetchAnalyticsActivity
|
||||||
|
} from '../api';
|
||||||
|
|
||||||
|
function BarChart({ data, height = 200 }) {
|
||||||
|
if (!data.length) return <p className="text-gray-400 text-sm">Keine Daten vorhanden</p>;
|
||||||
|
const max = Math.max(...data.map(d => d.count), 1);
|
||||||
|
return (
|
||||||
|
<div className="flex items-end gap-1" style={{ height }}>
|
||||||
|
{data.map((d, i) => (
|
||||||
|
<div key={i} className="flex-1 flex flex-col items-center gap-1">
|
||||||
|
<span className="text-xs text-gray-500">{d.count}</span>
|
||||||
|
<div
|
||||||
|
className="w-full bg-blue-500 rounded-t"
|
||||||
|
style={{ height: `${(d.count / max) * (height - 30)}px`, minHeight: d.count > 0 ? '4px' : '0' }}
|
||||||
|
title={`${d.date}: ${d.count}`}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-400" style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)', fontSize: '10px' }}>
|
||||||
|
{d.date.slice(5)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatCard({ label, value, color = 'blue' }) {
|
||||||
|
const colors = {
|
||||||
|
blue: 'text-blue-600',
|
||||||
|
green: 'text-green-600',
|
||||||
|
orange: 'text-orange-500',
|
||||||
|
red: 'text-red-600',
|
||||||
|
purple: 'text-purple-600',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
|
||||||
|
<p className="text-sm text-gray-500">{label}</p>
|
||||||
|
<p className={`text-3xl font-bold ${colors[color]}`}>{value}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const EVENT_LABELS = {
|
||||||
|
page_view: 'Seitenaufruf',
|
||||||
|
recipe_created: 'Rezept erstellt',
|
||||||
|
recipe_updated: 'Rezept bearbeitet',
|
||||||
|
recipe_deleted: 'Rezept gelöscht',
|
||||||
|
user_login: 'Login',
|
||||||
|
user_register: 'Registrierung',
|
||||||
|
};
|
||||||
|
|
||||||
|
const EVENT_COLORS = {
|
||||||
|
page_view: 'bg-blue-100 text-blue-700',
|
||||||
|
recipe_created: 'bg-green-100 text-green-700',
|
||||||
|
recipe_updated: 'bg-yellow-100 text-yellow-700',
|
||||||
|
recipe_deleted: 'bg-red-100 text-red-700',
|
||||||
|
user_login: 'bg-purple-100 text-purple-700',
|
||||||
|
user_register: 'bg-indigo-100 text-indigo-700',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Analytics() {
|
||||||
|
const { token } = useAuth();
|
||||||
|
const [overview, setOverview] = useState(null);
|
||||||
|
const [pageviews, setPageviews] = useState([]);
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
const [activity, setActivity] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
||||||
|
const [ov, pv, us, act] = await Promise.all([
|
||||||
|
fetchAnalyticsOverview(token),
|
||||||
|
fetchAnalyticsPageviews(token, thirtyDaysAgo),
|
||||||
|
fetchAnalyticsUsers(token),
|
||||||
|
fetchAnalyticsActivity(token),
|
||||||
|
]);
|
||||||
|
setOverview(ov);
|
||||||
|
setPageviews(pv);
|
||||||
|
setUsers(us);
|
||||||
|
setActivity(act);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Analytics Fehler:', e);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) return <div className="text-center py-12 text-gray-400">Laden...</div>;
|
||||||
|
if (!overview) return <div className="text-center py-12 text-gray-400">Fehler beim Laden der Analytics-Daten.</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-6xl mx-auto space-y-8">
|
||||||
|
<h1 className="text-2xl font-bold">Analytics Dashboard</h1>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||||
|
<StatCard label="Pageviews heute" value={overview.pageviews.today} color="blue" />
|
||||||
|
<StatCard label="Pageviews gestern" value={overview.pageviews.yesterday} color="blue" />
|
||||||
|
<StatCard label="Pageviews 7 Tage" value={overview.pageviews.week} color="purple" />
|
||||||
|
<StatCard label="Aktive User heute" value={overview.activeUsers.today} color="green" />
|
||||||
|
<StatCard label="Rezepte erstellt" value={overview.recipes.created} color="green" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<StatCard label="Rezepte bearbeitet" value={overview.recipes.updated} color="orange" />
|
||||||
|
<StatCard label="Rezepte gelöscht" value={overview.recipes.deleted} color="red" />
|
||||||
|
<StatCard label="Logins gesamt" value={overview.logins} color="purple" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">Pageviews (letzte 30 Tage)</h2>
|
||||||
|
<BarChart data={pageviews} height={220} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">Aktivste User</h2>
|
||||||
|
{users.length === 0 ? (
|
||||||
|
<p className="text-gray-400 text-sm">Keine Daten vorhanden.</p>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-gray-200">
|
||||||
|
<th className="text-left py-2 font-medium text-gray-500">User</th>
|
||||||
|
<th className="text-right py-2 font-medium text-gray-500">Logins</th>
|
||||||
|
<th className="text-right py-2 font-medium text-gray-500">Erstellt</th>
|
||||||
|
<th className="text-right py-2 font-medium text-gray-500">Bearbeitet</th>
|
||||||
|
<th className="text-right py-2 font-medium text-gray-500">Gesamt</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{users.map(u => (
|
||||||
|
<tr key={u.user_id} className="border-b border-gray-100">
|
||||||
|
<td className="py-2 font-medium">{u.username}</td>
|
||||||
|
<td className="py-2 text-right">{u.logins}</td>
|
||||||
|
<td className="py-2 text-right">{u.recipes_created}</td>
|
||||||
|
<td className="py-2 text-right">{u.recipes_updated}</td>
|
||||||
|
<td className="py-2 text-right font-semibold">{u.total_events}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">Letzte Aktivitäten</h2>
|
||||||
|
{activity.length === 0 ? (
|
||||||
|
<p className="text-gray-400 text-sm">Keine Aktivitäten vorhanden.</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||||
|
{activity.map(e => (
|
||||||
|
<div key={e.id} className="flex items-center gap-3 text-sm py-1 border-b border-gray-50">
|
||||||
|
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${EVENT_COLORS[e.type] || 'bg-gray-100 text-gray-600'}`}>
|
||||||
|
{EVENT_LABELS[e.type] || e.type}
|
||||||
|
</span>
|
||||||
|
<span className="flex-1 text-gray-600">
|
||||||
|
{e.username || 'Anonym'}
|
||||||
|
{e.path && <span className="text-gray-400 ml-1">{e.path}</span>}
|
||||||
|
{e.recipe_name && <span className="text-gray-400 ml-1">"{e.recipe_name}"</span>}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-400 whitespace-nowrap">
|
||||||
|
{new Date(e.timestamp).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user