QR-master/src/app/(app)/dashboard/page.tsx

504 lines
16 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import { StatsGrid } from '@/components/dashboard/StatsGrid';
import { QRCodeCard } from '@/components/dashboard/QRCodeCard';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf';
import { showToast } from '@/components/ui/Toast';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/Dialog';
interface QRCodeData {
id: string;
title: string;
type: 'STATIC' | 'DYNAMIC';
contentType: string;
content?: any;
slug: string;
status: 'ACTIVE' | 'PAUSED';
createdAt: string;
scans: number;
style?: any;
}
export default function DashboardPage() {
const { t } = useTranslation();
const router = useRouter();
const searchParams = useSearchParams();
const { fetchWithCsrf } = useCsrf();
const [qrCodes, setQrCodes] = useState<QRCodeData[]>([]);
const [loading, setLoading] = useState(true);
const [userPlan, setUserPlan] = useState<string>('FREE');
const [showUpgradeDialog, setShowUpgradeDialog] = useState(false);
const [upgradedPlan, setUpgradedPlan] = useState<string>('');
const [deletingAll, setDeletingAll] = useState(false);
const [stats, setStats] = useState({
totalScans: 0,
activeQRCodes: 0,
conversionRate: 0,
});
const mockQRCodes = [
{
id: '1',
title: 'Support Phone',
type: 'DYNAMIC' as const,
contentType: 'PHONE',
slug: 'support-phone-demo',
status: 'ACTIVE' as const,
createdAt: '2025-08-07T10:00:00Z',
scans: 0,
},
{
id: '2',
title: 'Event Details',
type: 'DYNAMIC' as const,
contentType: 'URL',
slug: 'event-details-demo',
status: 'ACTIVE' as const,
createdAt: '2025-08-07T10:01:00Z',
scans: 0,
},
{
id: '3',
title: 'Product Demo',
type: 'DYNAMIC' as const,
contentType: 'URL',
slug: 'product-demo-qr',
status: 'ACTIVE' as const,
createdAt: '2025-08-07T10:02:00Z',
scans: 0,
},
{
id: '4',
title: 'Company Website',
type: 'DYNAMIC' as const,
contentType: 'URL',
slug: 'company-website-qr',
status: 'ACTIVE' as const,
createdAt: '2025-08-07T10:03:00Z',
scans: 0,
},
{
id: '5',
title: 'Contact Email',
type: 'DYNAMIC' as const,
contentType: 'EMAIL',
slug: 'contact-email-qr',
status: 'ACTIVE' as const,
createdAt: '2025-08-07T10:04:00Z',
scans: 0,
},
{
id: '6',
title: 'Event Details',
type: 'DYNAMIC' as const,
contentType: 'URL',
slug: 'event-details-dup',
status: 'ACTIVE' as const,
createdAt: '2025-08-07T10:05:00Z',
scans: 0,
},
];
const blogPosts = [
{
title: 'QR-Codes im Restaurant: Die digitale Revolution der Speisekarte',
excerpt: 'Erfahren Sie, wie QR-Codes die Gastronomie revolutionieren...',
readTime: '5 Min',
slug: 'qr-codes-im-restaurant',
},
{
title: 'Dynamische vs. Statische QR-Codes: Was ist der Unterschied?',
excerpt: 'Ein umfassender Vergleich zwischen dynamischen und statischen QR-Codes...',
readTime: '3 Min',
slug: 'dynamische-vs-statische-qr-codes',
},
{
title: 'QR-Code Marketing-Strategien für 2025',
excerpt: 'Die besten Marketing-Strategien mit QR-Codes für Ihr Unternehmen...',
readTime: '7 Min',
slug: 'qr-code-marketing-strategien',
},
];
// Check for successful payment and verify session
useEffect(() => {
const success = searchParams.get('success');
if (success === 'true') {
const verifySession = async () => {
try {
const response = await fetch('/api/stripe/verify-session', {
method: 'POST',
});
if (response.ok) {
const data = await response.json();
setUserPlan(data.plan);
setUpgradedPlan(data.plan);
setShowUpgradeDialog(true);
// Remove success parameter from URL
router.replace('/dashboard');
} else {
console.error('Failed to verify session:', await response.text());
}
} catch (error) {
console.error('Error verifying session:', error);
}
};
verifySession();
}
}, [searchParams, router]);
useEffect(() => {
// Load real QR codes and user plan from API
const fetchData = async () => {
try {
// Fetch QR codes
const qrResponse = await fetch('/api/qrs');
if (qrResponse.ok) {
const data = await qrResponse.json();
setQrCodes(data);
// Calculate real stats
const totalScans = data.reduce((sum: number, qr: QRCodeData) => sum + (qr.scans || 0), 0);
const activeQRCodes = data.filter((qr: QRCodeData) => qr.status === 'ACTIVE').length;
const conversionRate = activeQRCodes > 0 ? Math.round((totalScans / (activeQRCodes * 100)) * 100) : 0;
setStats({
totalScans,
activeQRCodes,
conversionRate: Math.min(conversionRate, 100), // Cap at 100%
});
} else {
// If not logged in, show zeros
setQrCodes([]);
setStats({
totalScans: 0,
activeQRCodes: 0,
conversionRate: 0,
});
}
// Fetch user plan (using cookie-based auth, no session needed)
const userResponse = await fetch('/api/user/plan');
if (userResponse.ok) {
const userData = await userResponse.json();
setUserPlan(userData.plan || 'FREE');
}
} catch (error) {
console.error('Error fetching data:', error);
setQrCodes([]);
setStats({
totalScans: 0,
activeQRCodes: 0,
conversionRate: 0,
});
} finally {
setLoading(false);
}
};
fetchData();
}, []);
const handleEdit = (id: string) => {
// Redirect to edit page
router.push(`/qr/${id}/edit`);
};
const handlePause = async (id: string) => {
try {
const qr = qrCodes.find(q => q.id === id);
if (!qr) return;
const newStatus = qr.status === 'ACTIVE' ? 'PAUSED' : 'ACTIVE';
const response = await fetch(`/api/qrs/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ status: newStatus }),
});
if (response.ok) {
// Update local state
setQrCodes(qrCodes.map(q =>
q.id === id ? { ...q, status: newStatus } : q
));
showToast(`QR code ${newStatus === 'ACTIVE' ? 'resumed' : 'paused'}!`, 'success');
} else {
throw new Error('Failed to update status');
}
} catch (error) {
console.error('Error updating QR status:', error);
showToast('Failed to update QR code status', 'error');
}
};
const handleDelete = async (id: string) => {
if (!confirm('Are you sure you want to delete this QR code? This action cannot be undone.')) {
return;
}
try {
const response = await fetch(`/api/qrs/${id}`, {
method: 'DELETE',
});
if (response.ok) {
// Remove from local state
setQrCodes(qrCodes.filter(q => q.id !== id));
showToast('QR code deleted successfully!', 'success');
} else {
throw new Error('Failed to delete');
}
} catch (error) {
console.error('Error deleting QR:', error);
showToast('Failed to delete QR code', 'error');
}
};
const handleDeleteAll = async () => {
if (!confirm('Are you sure you want to delete ALL QR codes? This action cannot be undone.')) {
return;
}
// Double confirmation
if (!confirm('This will permanently delete ALL your QR codes. Are you absolutely sure?')) {
return;
}
setDeletingAll(true);
try {
const response = await fetchWithCsrf('/api/qrs/delete-all', {
method: 'DELETE',
});
if (response.ok) {
const data = await response.json();
setQrCodes([]);
setStats({
totalScans: 0,
activeQRCodes: 0,
conversionRate: 0,
});
showToast(`Successfully deleted ${data.deletedCount} QR code${data.deletedCount !== 1 ? 's' : ''}`, 'success');
} else {
throw new Error('Failed to delete all QR codes');
}
} catch (error) {
console.error('Error deleting all QR codes:', error);
showToast('Failed to delete QR codes', 'error');
} finally {
setDeletingAll(false);
}
};
const getPlanBadgeColor = (plan: string) => {
switch (plan) {
case 'PRO':
return 'info';
case 'BUSINESS':
return 'warning';
default:
return 'default';
}
};
const getPlanEmoji = (plan: string) => {
// No emojis anymore
return '';
};
return (
<div className="space-y-8">
{/* Header with Plan Badge */}
<div className="flex items-start justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">{t('dashboard.title')}</h1>
<p className="text-gray-600 mt-2">{t('dashboard.subtitle')}</p>
</div>
<div className="flex items-center space-x-3">
<Badge variant={getPlanBadgeColor(userPlan)} className="text-lg px-4 py-2">
{userPlan} Plan
</Badge>
{userPlan === 'FREE' && (
<Link href="/pricing">
<Button variant="primary">Upgrade</Button>
</Link>
)}
</div>
</div>
{/* Stats Grid */}
<StatsGrid stats={stats} />
{/* Recent QR Codes */}
<div>
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-gray-900">{t('dashboard.recent_codes')}</h2>
<div className="flex gap-3">
{qrCodes.length > 0 && (
<Button
variant="outline"
onClick={handleDeleteAll}
disabled={deletingAll}
className="border-red-600 text-red-600 hover:bg-red-50"
>
{deletingAll ? 'Deleting...' : 'Delete All'}
</Button>
)}
<Link href="/create">
<Button>Create New QR Code</Button>
</Link>
</div>
</div>
{loading ? (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{[1, 2, 3, 4, 5, 6].map((i) => (
<Card key={i}>
<CardContent className="p-4">
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-3"></div>
<div className="h-24 bg-gray-200 rounded mb-3"></div>
<div className="space-y-2">
<div className="h-3 bg-gray-200 rounded"></div>
<div className="h-3 bg-gray-200 rounded"></div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
) : (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{qrCodes.map((qr) => (
<QRCodeCard
key={qr.id}
qr={qr}
onEdit={handleEdit}
onPause={handlePause}
onDelete={handleDelete}
/>
))}
</div>
)}
</div>
{/* Blog & Resources */}
<div>
<h2 className="text-xl font-semibold text-gray-900 mb-6">{t('dashboard.blog_resources')}</h2>
<div className="grid md:grid-cols-3 gap-6">
{blogPosts.map((post) => (
<Card key={post.slug} hover>
<CardHeader>
<div className="flex items-center justify-between mb-2">
<Badge variant="info">{post.readTime}</Badge>
</div>
<CardTitle className="text-lg">{post.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600 text-sm">{post.excerpt}</p>
<Link
href={`/blog/${post.slug}`}
className="text-primary-600 hover:text-primary-700 text-sm font-medium mt-3 inline-block"
>
Read more
</Link>
</CardContent>
</Card>
))}
</div>
</div>
{/* Upgrade Success Dialog */}
<Dialog open={showUpgradeDialog} onOpenChange={setShowUpgradeDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-2xl text-center">
Upgrade erfolgreich!
</DialogTitle>
<DialogDescription className="text-center text-base pt-4">
Willkommen im <strong>{upgradedPlan} Plan</strong>! Ihr Konto wurde erfolgreich aktualisiert.
</DialogDescription>
</DialogHeader>
<div className="py-6 space-y-4">
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 p-6 rounded-lg border border-blue-200">
<h3 className="font-semibold text-gray-900 mb-3">Ihre neuen Features:</h3>
<ul className="space-y-2 text-sm text-gray-700">
{upgradedPlan === 'PRO' && (
<>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>50 dynamische QR-Codes</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>Branding (Farben anpassen)</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>Detaillierte Analytics (Devices, Locations, Time-Series)</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>CSV-Export</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>SVG/PNG Download</span>
</li>
</>
)}
{upgradedPlan === 'BUSINESS' && (
<>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>500 dynamische QR-Codes</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>Alles von Pro</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>Bulk QR-Generierung (bis 1,000)</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span>Prioritäts-Support</span>
</li>
</>
)}
</ul>
</div>
</div>
<DialogFooter>
<Button
variant="primary"
onClick={() => {
setShowUpgradeDialog(false);
router.push('/create');
}}
className="w-full"
>
Ersten QR-Code erstellen
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}