SEO: Fix structured data validation errors, delete static sitemap, and update indexing scripts
This commit is contained in:
570
src/app/(main)/(app)/dashboard/page.tsx
Normal file
570
src/app/(main)/(app)/dashboard/page.tsx
Normal file
@@ -0,0 +1,570 @@
|
||||
'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;
|
||||
createdAt: string;
|
||||
scans: number;
|
||||
style?: any;
|
||||
status?: 'ACTIVE' | 'INACTIVE';
|
||||
}
|
||||
|
||||
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,
|
||||
uniqueScans: 0,
|
||||
});
|
||||
const [analyticsData, setAnalyticsData] = useState<any>(null);
|
||||
|
||||
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 Card',
|
||||
type: 'DYNAMIC' as const,
|
||||
contentType: 'VCARD',
|
||||
slug: 'contact-card-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 = [
|
||||
// NEW POSTS
|
||||
{
|
||||
title: 'How to Create a QR Code for Restaurant Menu',
|
||||
excerpt: 'Step-by-step guide to creating digital menu QR codes for your restaurant. Best practices for touchless menus.',
|
||||
readTime: '12 Min',
|
||||
slug: 'qr-code-restaurant-menu',
|
||||
},
|
||||
{
|
||||
title: 'Free vCard QR Code Generator: Digital Business Cards',
|
||||
excerpt: 'Create professional vCard QR codes for digital business cards. Share contact info instantly.',
|
||||
readTime: '10 Min',
|
||||
slug: 'vcard-qr-code-generator',
|
||||
},
|
||||
{
|
||||
title: 'Best QR Code Generator for Small Business',
|
||||
excerpt: 'Find the best QR code solution for your small business. Compare features, pricing, and use cases.',
|
||||
readTime: '14 Min',
|
||||
slug: 'qr-code-small-business',
|
||||
},
|
||||
{
|
||||
title: 'QR Code Print Size Guide',
|
||||
excerpt: 'Complete guide to QR code print sizes. Minimum dimensions for business cards, posters, and more.',
|
||||
readTime: '8 Min',
|
||||
slug: 'qr-code-print-size-guide',
|
||||
},
|
||||
// EXISTING POSTS
|
||||
{
|
||||
title: 'QR Code Tracking: Complete Guide 2025',
|
||||
excerpt: 'Learn how to track QR code scans with real-time analytics. Compare free vs paid tracking tools.',
|
||||
readTime: '12 Min',
|
||||
slug: 'qr-code-tracking-guide-2025',
|
||||
},
|
||||
{
|
||||
title: 'Dynamic vs Static QR Codes: Which Should You Use?',
|
||||
excerpt: 'Understand the difference between static and dynamic QR codes. Pros, cons, and when to use each.',
|
||||
readTime: '10 Min',
|
||||
slug: 'dynamic-vs-static-qr-codes',
|
||||
},
|
||||
{
|
||||
title: 'How to Generate Bulk QR Codes from Excel',
|
||||
excerpt: 'Generate hundreds of QR codes from Excel or CSV files in minutes. Step-by-step guide.',
|
||||
readTime: '13 Min',
|
||||
slug: 'bulk-qr-code-generator-excel',
|
||||
},
|
||||
{
|
||||
title: 'QR Code Analytics: Track, Measure & Optimize',
|
||||
excerpt: 'Learn how to leverage scan analytics and dashboard insights to maximize QR code ROI.',
|
||||
readTime: '15 Min',
|
||||
slug: 'qr-code-analytics',
|
||||
},
|
||||
];
|
||||
|
||||
// Track Google OAuth login/signup
|
||||
useEffect(() => {
|
||||
const authMethod = searchParams.get('authMethod');
|
||||
const isNewUser = searchParams.get('isNewUser') === 'true';
|
||||
|
||||
if (authMethod === 'google') {
|
||||
const trackGoogleAuth = async () => {
|
||||
try {
|
||||
// Fetch user data from API (cookie-based auth)
|
||||
const response = await fetch('/api/user');
|
||||
if (!response.ok) return;
|
||||
|
||||
const user = await response.json();
|
||||
|
||||
// Store in localStorage for consistency
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
|
||||
const { identifyUser, trackEvent } = await import('@/components/PostHogProvider');
|
||||
identifyUser(user.id, {
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
plan: user.plan || 'FREE',
|
||||
provider: 'google',
|
||||
});
|
||||
|
||||
trackEvent(isNewUser ? 'user_signup' : 'user_login', {
|
||||
method: 'google',
|
||||
email: user.email,
|
||||
isNewUser,
|
||||
});
|
||||
|
||||
// Clean up URL params
|
||||
router.replace('/dashboard');
|
||||
} catch (error) {
|
||||
console.error('PostHog tracking error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
trackGoogleAuth();
|
||||
}
|
||||
}, [searchParams, router]);
|
||||
|
||||
// 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;
|
||||
// Calculate unique scans (absolute count)
|
||||
const uniqueScans = data.reduce((acc: number, qr: any) => acc + (qr.uniqueScans || 0), 0);
|
||||
const conversionRate = totalScans > 0 ? Math.round((uniqueScans / totalScans) * 100) : 0;
|
||||
|
||||
setStats({
|
||||
totalScans,
|
||||
activeQRCodes,
|
||||
conversionRate,
|
||||
uniqueScans,
|
||||
});
|
||||
} else {
|
||||
// If not logged in, show zeros
|
||||
setQrCodes([]);
|
||||
setStats({
|
||||
totalScans: 0,
|
||||
activeQRCodes: 0,
|
||||
conversionRate: 0,
|
||||
uniqueScans: 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');
|
||||
}
|
||||
|
||||
// Fetch analytics data for trends (last 30 days = month comparison)
|
||||
const analyticsResponse = await fetch('/api/analytics/summary?range=30');
|
||||
if (analyticsResponse.ok) {
|
||||
const analytics = await analyticsResponse.json();
|
||||
setAnalyticsData(analytics);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
setQrCodes([]);
|
||||
setStats({
|
||||
totalScans: 0,
|
||||
activeQRCodes: 0,
|
||||
conversionRate: 0,
|
||||
uniqueScans: 0,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const handleEdit = (id: string) => {
|
||||
// Redirect to edit page
|
||||
router.push(`/qr/${id}/edit`);
|
||||
};
|
||||
|
||||
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 fetchWithCsrf(`/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,
|
||||
uniqueScans: 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}
|
||||
trends={{
|
||||
totalScans: analyticsData?.summary.scansTrend,
|
||||
comparisonPeriod: analyticsData?.summary.comparisonPeriod || 'month'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 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}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Blog & Resources - Horizontal Scroll */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-6">{t('dashboard.blog_resources')}</h2>
|
||||
<div className="overflow-x-auto pb-4 -mx-4 px-4">
|
||||
<div className="flex gap-6" style={{ minWidth: 'max-content' }}>
|
||||
{blogPosts.map((post) => (
|
||||
<Card key={post.slug} hover className="flex-shrink-0" style={{ width: '300px' }}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Badge variant="info">{post.readTime}</Badge>
|
||||
</div>
|
||||
<CardTitle className="text-lg line-clamp-2">{post.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-600 text-sm line-clamp-2">{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>
|
||||
</div>
|
||||
|
||||
{/* Upgrade Success Dialog */}
|
||||
<Dialog open={showUpgradeDialog} onOpenChange={setShowUpgradeDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl text-center">
|
||||
Upgrade Successful!
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-center text-base pt-4">
|
||||
Welcome to the <strong>{upgradedPlan} Plan</strong>! Your account has been successfully upgraded.
|
||||
</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">Your New 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 Dynamic QR Codes</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span>Custom Branding (Colors & Logo)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span>Detailed 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 Dynamic QR Codes</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span>Everything from Pro</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span>Bulk QR Generation (up to 1,000)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-green-600 mr-2">✓</span>
|
||||
<span>Priority Support</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setShowUpgradeDialog(false);
|
||||
router.push('/create');
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
Create First QR Code
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user