SEO: Fix structured data validation errors, delete static sitemap, and update indexing scripts
This commit is contained in:
26
src/app/(main)/vcard/layout.tsx
Normal file
26
src/app/(main)/vcard/layout.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
import '@/styles/globals.css';
|
||||
|
||||
export const metadata = {
|
||||
title: 'vCard Download',
|
||||
description: 'Download contact information',
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/favicon.svg', type: 'image/svg+xml' },
|
||||
{ url: '/logo.svg', type: 'image/svg+xml' },
|
||||
],
|
||||
apple: '/logo.svg',
|
||||
},
|
||||
};
|
||||
|
||||
export default function VCardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
274
src/app/(main)/vcard/page.tsx
Normal file
274
src/app/(main)/vcard/page.tsx
Normal file
@@ -0,0 +1,274 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function VCardPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [firstName, setFirstName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [organization, setOrganization] = useState('');
|
||||
const [title, setTitle] = useState('');
|
||||
const [hasAutoDownloaded, setHasAutoDownloaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const firstNameParam = searchParams.get('firstName');
|
||||
const lastNameParam = searchParams.get('lastName');
|
||||
const emailParam = searchParams.get('email');
|
||||
const phoneParam = searchParams.get('phone');
|
||||
const organizationParam = searchParams.get('organization');
|
||||
const titleParam = searchParams.get('title');
|
||||
|
||||
if (firstNameParam) setFirstName(firstNameParam);
|
||||
if (lastNameParam) setLastName(lastNameParam);
|
||||
if (emailParam) setEmail(emailParam);
|
||||
if (phoneParam) setPhone(phoneParam);
|
||||
if (organizationParam) setOrganization(organizationParam);
|
||||
if (titleParam) setTitle(titleParam);
|
||||
|
||||
setIsLoading(false);
|
||||
}, [searchParams]);
|
||||
|
||||
// Auto-download after 500ms (only once)
|
||||
useEffect(() => {
|
||||
if ((firstName || lastName) && !hasAutoDownloaded) {
|
||||
const timer = setTimeout(() => {
|
||||
handleSaveContact();
|
||||
setHasAutoDownloaded(true);
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [firstName, lastName, hasAutoDownloaded]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleSaveContact = () => {
|
||||
// Generate vCard format
|
||||
const vCard = `BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
FN:${firstName} ${lastName}
|
||||
N:${lastName};${firstName};;;
|
||||
${organization ? `ORG:${organization}` : ''}
|
||||
${title ? `TITLE:${title}` : ''}
|
||||
${email ? `EMAIL:${email}` : ''}
|
||||
${phone ? `TEL:${phone}` : ''}
|
||||
END:VCARD`;
|
||||
|
||||
// Create a blob and download
|
||||
const blob = new Blob([vCard], { type: 'text/vcard;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${firstName}_${lastName}.vcf`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// Show loading or error state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
backgroundColor: '#ffffff'
|
||||
}}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '48px', marginBottom: '16px' }}>👤</div>
|
||||
<p style={{ color: '#666' }}>Loading contact...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!firstName && !lastName) {
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
padding: '20px',
|
||||
backgroundColor: '#ffffff'
|
||||
}}>
|
||||
<div style={{
|
||||
maxWidth: '420px',
|
||||
width: '100%',
|
||||
padding: '48px 32px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '64px',
|
||||
marginBottom: '24px',
|
||||
opacity: 0.3
|
||||
}}>👤</div>
|
||||
<h1 style={{
|
||||
fontSize: '22px',
|
||||
fontWeight: '600',
|
||||
marginBottom: '12px',
|
||||
color: '#1a1a1a'
|
||||
}}>No Contact Found</h1>
|
||||
<p style={{
|
||||
color: '#666',
|
||||
fontSize: '15px',
|
||||
lineHeight: '1.6'
|
||||
}}>This QR code doesn't contain any contact information.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
padding: '20px',
|
||||
backgroundColor: '#ffffff'
|
||||
}}>
|
||||
<div style={{
|
||||
maxWidth: '420px',
|
||||
width: '100%',
|
||||
padding: '48px 32px'
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
|
||||
<div style={{
|
||||
width: '80px',
|
||||
height: '80px',
|
||||
margin: '0 auto 20px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#f0f0f0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '36px'
|
||||
}}>👤</div>
|
||||
|
||||
<h1 style={{
|
||||
fontSize: '28px',
|
||||
fontWeight: '600',
|
||||
marginBottom: '8px',
|
||||
color: '#1a1a1a',
|
||||
letterSpacing: '-0.02em'
|
||||
}}>
|
||||
{firstName} {lastName}
|
||||
</h1>
|
||||
|
||||
{(title || organization) && (
|
||||
<p style={{
|
||||
color: '#666',
|
||||
fontSize: '16px',
|
||||
marginTop: '4px'
|
||||
}}>
|
||||
{title && organization && `${title} at ${organization}`}
|
||||
{title && !organization && title}
|
||||
{!title && organization && organization}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Contact Details */}
|
||||
<div style={{ marginBottom: '32px' }}>
|
||||
{email && (
|
||||
<div style={{
|
||||
padding: '16px 20px',
|
||||
backgroundColor: '#f8f8f8',
|
||||
borderRadius: '12px',
|
||||
marginBottom: '12px',
|
||||
border: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: '#888',
|
||||
marginBottom: '6px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
fontWeight: '500'
|
||||
}}>Email</div>
|
||||
<a href={`mailto:${email}`} style={{
|
||||
fontSize: '15px',
|
||||
fontWeight: '500',
|
||||
color: '#2563eb',
|
||||
textDecoration: 'none',
|
||||
wordBreak: 'break-all'
|
||||
}}>{email}</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phone && (
|
||||
<div style={{
|
||||
padding: '16px 20px',
|
||||
backgroundColor: '#f8f8f8',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: '#888',
|
||||
marginBottom: '6px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
fontWeight: '500'
|
||||
}}>Phone</div>
|
||||
<a href={`tel:${phone}`} style={{
|
||||
fontSize: '15px',
|
||||
fontWeight: '500',
|
||||
color: '#2563eb',
|
||||
textDecoration: 'none'
|
||||
}}>{phone}</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<button
|
||||
onClick={handleSaveContact}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '16px',
|
||||
fontSize: '16px',
|
||||
fontWeight: '600',
|
||||
color: 'white',
|
||||
backgroundColor: '#2563eb',
|
||||
border: 'none',
|
||||
borderRadius: '12px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
boxShadow: '0 2px 8px rgba(37, 99, 235, 0.2)'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#1d4ed8';
|
||||
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(37, 99, 235, 0.3)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#2563eb';
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(37, 99, 235, 0.2)';
|
||||
}}
|
||||
>
|
||||
Save to Contacts
|
||||
</button>
|
||||
|
||||
<p style={{
|
||||
textAlign: 'center',
|
||||
fontSize: '13px',
|
||||
color: '#999',
|
||||
marginTop: '16px',
|
||||
lineHeight: '1.5'
|
||||
}}>
|
||||
Add this contact to your address book
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user