feat: Add professional portfolio images and Hero redesign
- Add 15 optimized WebP images for portfolio (21-53KB each) - Redesign Hero section with full-width background image - Add SSR support for Hero component - Update About section with new portrait image - Update Contact page with professional headshot - Add images to Services/Leistungen page - Add image optimization script (sharp) - Update translations for gallery section Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Loader2, BarChart2, Users, Eye, Clock, TrendingUp, TrendingDown, LogOut } from 'lucide-react';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
|
||||
interface MetricCard {
|
||||
title: string;
|
||||
value: string;
|
||||
subtitle: string;
|
||||
trend?: 'up' | 'down';
|
||||
trendValue?: string;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function DashboardContent() {
|
||||
const t = useTranslations('dashboard');
|
||||
const locale = useLocale();
|
||||
const router = useRouter();
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
const supabase = createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
if (!user) {
|
||||
router.push(`/${locale}/login`);
|
||||
return;
|
||||
}
|
||||
|
||||
setUser(user);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, [locale, router]);
|
||||
|
||||
const handleLogout = async () => {
|
||||
const supabase = createClient();
|
||||
await supabase.auth.signOut();
|
||||
router.push(`/${locale}`);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<Loader2 className="h-8 w-8 text-zinc-400 animate-spin" />
|
||||
<p className="text-zinc-400 text-sm">{t('status.loading')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<p className="text-zinc-400">{t('status.notAuthenticated')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const metrics: MetricCard[] = [
|
||||
{
|
||||
title: t('metrics.pageViews.title'),
|
||||
value: '12,543',
|
||||
subtitle: '45 ' + t('metrics.pageViews.perMinute'),
|
||||
trend: 'up',
|
||||
trendValue: '+12.5%',
|
||||
icon: <Eye className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: t('metrics.uniqueVisitors.title'),
|
||||
value: '3,721',
|
||||
subtitle: '127 ' + t('metrics.uniqueVisitors.currentlyActive'),
|
||||
trend: 'up',
|
||||
trendValue: '+8.2%',
|
||||
icon: <Users className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: t('metrics.conversions.title'),
|
||||
value: '156',
|
||||
subtitle: '4.2% ' + t('metrics.conversions.rate'),
|
||||
trend: 'down',
|
||||
trendValue: '-2.1%',
|
||||
icon: <BarChart2 className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: t('metrics.timeOnSite.title'),
|
||||
value: '4:32',
|
||||
subtitle: '32% ' + t('metrics.timeOnSite.bounceRate'),
|
||||
trend: 'up',
|
||||
trendValue: '+15.3%',
|
||||
icon: <Clock className="h-5 w-5" />,
|
||||
},
|
||||
];
|
||||
|
||||
const topPages = [
|
||||
{ path: '/', views: 4521, change: '+12%' },
|
||||
{ path: '/portfolio', views: 2341, change: '+8%' },
|
||||
{ path: '/about', views: 1823, change: '+5%' },
|
||||
{ path: '/blog', views: 1456, change: '-2%' },
|
||||
{ path: '/contact', views: 892, change: '+3%' },
|
||||
];
|
||||
|
||||
return (
|
||||
<main className="min-h-screen py-16 sm:py-20 lg:py-24">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-12">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">{t('header.title')}</h1>
|
||||
<p className="text-zinc-400 mt-2">
|
||||
127 {t('header.activeUsers')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-lg text-zinc-300 transition-colors"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Metrics Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
|
||||
{metrics.map((metric, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-zinc-900/50 backdrop-blur-sm rounded-xl p-6 ring-1 ring-zinc-800"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="p-2 bg-zinc-800 rounded-lg text-zinc-400">
|
||||
{metric.icon}
|
||||
</div>
|
||||
{metric.trend && (
|
||||
<div className={`flex items-center gap-1 text-sm ${
|
||||
metric.trend === 'up' ? 'text-green-500' : 'text-red-500'
|
||||
}`}>
|
||||
{metric.trend === 'up' ? (
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
) : (
|
||||
<TrendingDown className="h-4 w-4" />
|
||||
)}
|
||||
{metric.trendValue}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-sm text-zinc-400 mb-1">{metric.title}</h3>
|
||||
<p className="text-2xl font-bold text-white">{metric.value}</p>
|
||||
<p className="text-sm text-zinc-500 mt-1">{metric.subtitle}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Top Pages */}
|
||||
<div className="bg-zinc-900/50 backdrop-blur-sm rounded-xl ring-1 ring-zinc-800">
|
||||
<div className="p-6 border-b border-zinc-800">
|
||||
<h2 className="text-xl font-semibold text-white">{t('topPages.title')}</h2>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="text-left text-sm text-zinc-500 border-b border-zinc-800">
|
||||
<th className="px-6 py-4 font-medium">{t('topPages.columns.path')}</th>
|
||||
<th className="px-6 py-4 font-medium">{t('topPages.columns.views')}</th>
|
||||
<th className="px-6 py-4 font-medium">{t('topPages.columns.change')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{topPages.map((page, index) => (
|
||||
<tr key={index} className="border-b border-zinc-800/50 last:border-0">
|
||||
<td className="px-6 py-4 text-white font-mono text-sm">{page.path}</td>
|
||||
<td className="px-6 py-4 text-zinc-300">{page.views.toLocaleString()}</td>
|
||||
<td className={`px-6 py-4 ${
|
||||
page.change.startsWith('+') ? 'text-green-500' : 'text-red-500'
|
||||
}`}>
|
||||
{page.change}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user