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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Lock, Loader2 } from 'lucide-react';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
|
||||
export default function LoginForm() {
|
||||
const t = useTranslations('login');
|
||||
const locale = useLocale();
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const supabase = createClient();
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
router.push(`/${locale}/dashboard`);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t('form.errors.default'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen flex items-center justify-center py-24 px-4">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div className="text-center">
|
||||
<Lock className="mx-auto h-12 w-12 text-orange-500" />
|
||||
<h1 className="mt-6 text-3xl font-bold text-white">{t('header.title')}</h1>
|
||||
</div>
|
||||
|
||||
<form className="mt-8 space-y-6" onSubmit={handleLogin}>
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 text-red-500 p-4 rounded-lg text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-white/80 mb-2">
|
||||
{t('form.email.label')}
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder={t('form.email.placeholder')}
|
||||
className="w-full bg-zinc-900/50 border border-white/10 rounded-lg px-4 py-3 text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-white/80 mb-2">
|
||||
{t('form.password.label')}
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t('form.password.placeholder')}
|
||||
className="w-full bg-zinc-900/50 border border-white/10 rounded-lg px-4 py-3 text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-orange-500 hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed text-white font-medium py-3 rounded-lg transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
{t('form.submit.loading')}
|
||||
</>
|
||||
) : (
|
||||
t('form.submit.default')
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="text-center text-sm text-zinc-500">
|
||||
<p className="flex items-center justify-center gap-2">
|
||||
<svg className="h-4 w-4 text-green-500" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
||||
<path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{t('security.secureConnection')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as LoginForm } from './LoginForm';
|
||||
export { default as DashboardContent } from './DashboardContent';
|
||||
Reference in New Issue
Block a user