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