This commit is contained in:
Timo Knuth
2026-01-25 14:59:25 +01:00
parent eef4855c1b
commit 30c1e57eab
104 changed files with 24652 additions and 24741 deletions

View File

@@ -1,26 +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>
);
}
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>
);
}

View File

@@ -1,274 +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>
);
}
'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>
);
}