feat: Add QR code card component with download functionality and a new pricing page with internationalization.
This commit is contained in:
parent
e7478a4af7
commit
d04e7a1f70
|
|
@ -141,13 +141,13 @@ export default function PricingPage() {
|
|||
'50 dynamic QR codes',
|
||||
'Unlimited static QR codes',
|
||||
'Advanced analytics (scans, devices, locations)',
|
||||
'Custom branding (colors)',
|
||||
'Custom branding (colors & logos)',
|
||||
],
|
||||
buttonText: isCurrentPlanWithInterval('PRO', selectedInterval)
|
||||
? 'Current Plan'
|
||||
: hasPlanDifferentInterval('PRO')
|
||||
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
|
||||
: 'Upgrade to Pro',
|
||||
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
|
||||
: 'Upgrade to Pro',
|
||||
buttonVariant: 'primary' as const,
|
||||
disabled: isCurrentPlanWithInterval('PRO', selectedInterval),
|
||||
popular: true,
|
||||
|
|
@ -170,8 +170,8 @@ export default function PricingPage() {
|
|||
buttonText: isCurrentPlanWithInterval('BUSINESS', selectedInterval)
|
||||
? 'Current Plan'
|
||||
: hasPlanDifferentInterval('BUSINESS')
|
||||
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
|
||||
: 'Upgrade to Business',
|
||||
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
|
||||
: 'Upgrade to Business',
|
||||
buttonVariant: 'primary' as const,
|
||||
disabled: isCurrentPlanWithInterval('BUSINESS', selectedInterval),
|
||||
popular: false,
|
||||
|
|
|
|||
|
|
@ -1,240 +1,240 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { Card, CardContent } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Dropdown, DropdownItem } from '@/components/ui/Dropdown';
|
||||
import { formatDate } from '@/lib/utils';
|
||||
|
||||
interface QRCodeCardProps {
|
||||
qr: {
|
||||
id: string;
|
||||
title: string;
|
||||
type: 'STATIC' | 'DYNAMIC';
|
||||
contentType: string;
|
||||
content?: any;
|
||||
slug: string;
|
||||
createdAt: string;
|
||||
scans?: number;
|
||||
style?: any;
|
||||
};
|
||||
onEdit: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
export const QRCodeCard: React.FC<QRCodeCardProps> = ({
|
||||
qr,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}) => {
|
||||
// For dynamic QR codes, use the redirect URL for tracking
|
||||
// For static QR codes, use the direct URL from content
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || (typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3050');
|
||||
|
||||
// Get the QR URL based on type
|
||||
let qrUrl = '';
|
||||
|
||||
// SIMPLE FIX: For STATIC QR codes, ALWAYS use the direct content
|
||||
if (qr.type === 'STATIC') {
|
||||
// Extract the actual URL/content based on contentType
|
||||
if (qr.contentType === 'URL' && qr.content?.url) {
|
||||
qrUrl = qr.content.url;
|
||||
} else if (qr.contentType === 'PHONE' && qr.content?.phone) {
|
||||
qrUrl = `tel:${qr.content.phone}`;
|
||||
} else if (qr.contentType === 'VCARD') {
|
||||
// VCARD content needs to be formatted properly
|
||||
qrUrl = `BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
FN:${qr.content.firstName || ''} ${qr.content.lastName || ''}
|
||||
N:${qr.content.lastName || ''};${qr.content.firstName || ''};;;
|
||||
${qr.content.organization ? `ORG:${qr.content.organization}` : ''}
|
||||
${qr.content.title ? `TITLE:${qr.content.title}` : ''}
|
||||
${qr.content.email ? `EMAIL:${qr.content.email}` : ''}
|
||||
${qr.content.phone ? `TEL:${qr.content.phone}` : ''}
|
||||
END:VCARD`;
|
||||
} else if (qr.contentType === 'GEO' && qr.content) {
|
||||
const lat = qr.content.latitude || 0;
|
||||
const lon = qr.content.longitude || 0;
|
||||
const label = qr.content.label ? `?q=${encodeURIComponent(qr.content.label)}` : '';
|
||||
qrUrl = `geo:${lat},${lon}${label}`;
|
||||
} else if (qr.contentType === 'TEXT' && qr.content?.text) {
|
||||
qrUrl = qr.content.text;
|
||||
} else if (qr.content?.qrContent) {
|
||||
// Fallback to qrContent if it exists
|
||||
qrUrl = qr.content.qrContent;
|
||||
} else {
|
||||
// Last resort fallback
|
||||
qrUrl = `${baseUrl}/r/${qr.slug}`;
|
||||
}
|
||||
console.log(`STATIC QR [${qr.title}]: ${qrUrl}`);
|
||||
} else {
|
||||
// DYNAMIC QR codes always use redirect for tracking
|
||||
qrUrl = `${baseUrl}/r/${qr.slug}`;
|
||||
console.log(`DYNAMIC QR [${qr.title}]: ${qrUrl}`);
|
||||
}
|
||||
|
||||
const downloadQR = (format: 'png' | 'svg') => {
|
||||
const svg = document.querySelector(`#qr-${qr.id} svg`);
|
||||
if (!svg) return;
|
||||
|
||||
if (format === 'svg') {
|
||||
let svgData = new XMLSerializer().serializeToString(svg);
|
||||
|
||||
// If rounded corners, wrap in a clipped SVG
|
||||
if (qr.style?.cornerStyle === 'rounded') {
|
||||
const width = svg.getAttribute('width') || '96';
|
||||
const height = svg.getAttribute('height') || '96';
|
||||
const borderRadius = 10; // Smaller radius for dashboard
|
||||
|
||||
svgData = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||
<defs>
|
||||
<clipPath id="rounded-corners-${qr.id}">
|
||||
<rect x="0" y="0" width="${width}" height="${height}" rx="${borderRadius}" ry="${borderRadius}"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#rounded-corners-${qr.id})">
|
||||
${svgData}
|
||||
</g>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.svg`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} else {
|
||||
// Convert SVG to PNG
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const img = new Image();
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
img.onload = () => {
|
||||
canvas.width = 300;
|
||||
canvas.height = 300;
|
||||
|
||||
// Apply rounded corners if needed
|
||||
if (qr.style?.cornerStyle === 'rounded') {
|
||||
const borderRadius = 30; // Scale up for 300px canvas
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(borderRadius, 0);
|
||||
ctx.lineTo(300 - borderRadius, 0);
|
||||
ctx.quadraticCurveTo(300, 0, 300, borderRadius);
|
||||
ctx.lineTo(300, 300 - borderRadius);
|
||||
ctx.quadraticCurveTo(300, 300, 300 - borderRadius, 300);
|
||||
ctx.lineTo(borderRadius, 300);
|
||||
ctx.quadraticCurveTo(0, 300, 0, 300 - borderRadius);
|
||||
ctx.lineTo(0, borderRadius);
|
||||
ctx.quadraticCurveTo(0, 0, borderRadius, 0);
|
||||
ctx.closePath();
|
||||
ctx.clip();
|
||||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0, 300, 300);
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.png`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
});
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
img.src = url;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card hover>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-gray-900 mb-1">{qr.title}</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant={qr.type === 'DYNAMIC' ? 'info' : 'default'}>
|
||||
{qr.type}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dropdown
|
||||
align="right"
|
||||
trigger={
|
||||
<button className="p-1 hover:bg-gray-100 rounded">
|
||||
<svg className="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<DropdownItem onClick={() => downloadQR('png')}>Download PNG</DropdownItem>
|
||||
<DropdownItem onClick={() => downloadQR('svg')}>Download SVG</DropdownItem>
|
||||
{qr.type === 'DYNAMIC' && (
|
||||
<DropdownItem onClick={() => onEdit(qr.id)}>Edit</DropdownItem>
|
||||
)}
|
||||
<DropdownItem onClick={() => onDelete(qr.id)} className="text-red-600">
|
||||
Delete
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div id={`qr-${qr.id}`} className="flex items-center justify-center bg-gray-50 rounded-lg p-4 mb-3">
|
||||
<div className={qr.style?.cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}>
|
||||
<QRCodeSVG
|
||||
value={qrUrl}
|
||||
size={96}
|
||||
fgColor={qr.style?.foregroundColor || '#000000'}
|
||||
bgColor={qr.style?.backgroundColor || '#FFFFFF'}
|
||||
level="H"
|
||||
imageSettings={qr.style?.imageSettings ? {
|
||||
src: qr.style.imageSettings.src,
|
||||
height: qr.style.imageSettings.height * (96 / 200), // Scale logo for smaller QR
|
||||
width: qr.style.imageSettings.width * (96 / 200),
|
||||
excavate: qr.style.imageSettings.excavate,
|
||||
} : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-500">Type:</span>
|
||||
<span className="text-gray-900">{qr.contentType}</span>
|
||||
</div>
|
||||
{qr.type === 'DYNAMIC' && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-500">Scans:</span>
|
||||
<span className="text-gray-900">{qr.scans || 0}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-500">Created:</span>
|
||||
<span className="text-gray-900">{formatDate(qr.createdAt)}</span>
|
||||
</div>
|
||||
{qr.type === 'DYNAMIC' && (
|
||||
<div className="pt-2 border-t">
|
||||
<p className="text-xs text-gray-500">
|
||||
📊 Dynamic QR: Tracks scans via {baseUrl}/r/{qr.slug}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { Card, CardContent } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Dropdown, DropdownItem } from '@/components/ui/Dropdown';
|
||||
import { formatDate } from '@/lib/utils';
|
||||
|
||||
interface QRCodeCardProps {
|
||||
qr: {
|
||||
id: string;
|
||||
title: string;
|
||||
type: 'STATIC' | 'DYNAMIC';
|
||||
contentType: string;
|
||||
content?: any;
|
||||
slug: string;
|
||||
createdAt: string;
|
||||
scans?: number;
|
||||
style?: any;
|
||||
};
|
||||
onEdit: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
export const QRCodeCard: React.FC<QRCodeCardProps> = ({
|
||||
qr,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}) => {
|
||||
// For dynamic QR codes, use the redirect URL for tracking
|
||||
// For static QR codes, use the direct URL from content
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || (typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3050');
|
||||
|
||||
// Get the QR URL based on type
|
||||
let qrUrl = '';
|
||||
|
||||
// SIMPLE FIX: For STATIC QR codes, ALWAYS use the direct content
|
||||
if (qr.type === 'STATIC') {
|
||||
// Extract the actual URL/content based on contentType
|
||||
if (qr.contentType === 'URL' && qr.content?.url) {
|
||||
qrUrl = qr.content.url;
|
||||
} else if (qr.contentType === 'PHONE' && qr.content?.phone) {
|
||||
qrUrl = `tel:${qr.content.phone}`;
|
||||
} else if (qr.contentType === 'VCARD') {
|
||||
// VCARD content needs to be formatted properly
|
||||
qrUrl = `BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
FN:${qr.content.firstName || ''} ${qr.content.lastName || ''}
|
||||
N:${qr.content.lastName || ''};${qr.content.firstName || ''};;;
|
||||
${qr.content.organization ? `ORG:${qr.content.organization}` : ''}
|
||||
${qr.content.title ? `TITLE:${qr.content.title}` : ''}
|
||||
${qr.content.email ? `EMAIL:${qr.content.email}` : ''}
|
||||
${qr.content.phone ? `TEL:${qr.content.phone}` : ''}
|
||||
END:VCARD`;
|
||||
} else if (qr.contentType === 'GEO' && qr.content) {
|
||||
const lat = qr.content.latitude || 0;
|
||||
const lon = qr.content.longitude || 0;
|
||||
const label = qr.content.label ? `?q=${encodeURIComponent(qr.content.label)}` : '';
|
||||
qrUrl = `geo:${lat},${lon}${label}`;
|
||||
} else if (qr.contentType === 'TEXT' && qr.content?.text) {
|
||||
qrUrl = qr.content.text;
|
||||
} else if (qr.content?.qrContent) {
|
||||
// Fallback to qrContent if it exists
|
||||
qrUrl = qr.content.qrContent;
|
||||
} else {
|
||||
// Last resort fallback
|
||||
qrUrl = `${baseUrl}/r/${qr.slug}`;
|
||||
}
|
||||
console.log(`STATIC QR [${qr.title}]: ${qrUrl}`);
|
||||
} else {
|
||||
// DYNAMIC QR codes always use redirect for tracking
|
||||
qrUrl = `${baseUrl}/r/${qr.slug}`;
|
||||
console.log(`DYNAMIC QR [${qr.title}]: ${qrUrl}`);
|
||||
}
|
||||
|
||||
const downloadQR = (format: 'png' | 'svg') => {
|
||||
const svg = document.querySelector(`#qr-${qr.id} svg`);
|
||||
if (!svg) return;
|
||||
|
||||
if (format === 'svg') {
|
||||
let svgData = new XMLSerializer().serializeToString(svg);
|
||||
|
||||
// If rounded corners, wrap in a clipped SVG
|
||||
if (qr.style?.cornerStyle === 'rounded') {
|
||||
const width = svg.getAttribute('width') || '96';
|
||||
const height = svg.getAttribute('height') || '96';
|
||||
const borderRadius = 10; // Smaller radius for dashboard
|
||||
|
||||
svgData = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||
<defs>
|
||||
<clipPath id="rounded-corners-${qr.id}">
|
||||
<rect x="0" y="0" width="${width}" height="${height}" rx="${borderRadius}" ry="${borderRadius}"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#rounded-corners-${qr.id})">
|
||||
${svgData}
|
||||
</g>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.svg`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} else {
|
||||
// Convert SVG to PNG
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const img = new Image();
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
img.onload = () => {
|
||||
canvas.width = 300;
|
||||
canvas.height = 300;
|
||||
|
||||
// Apply rounded corners if needed
|
||||
if (qr.style?.cornerStyle === 'rounded') {
|
||||
const borderRadius = 30; // Scale up for 300px canvas
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(borderRadius, 0);
|
||||
ctx.lineTo(300 - borderRadius, 0);
|
||||
ctx.quadraticCurveTo(300, 0, 300, borderRadius);
|
||||
ctx.lineTo(300, 300 - borderRadius);
|
||||
ctx.quadraticCurveTo(300, 300, 300 - borderRadius, 300);
|
||||
ctx.lineTo(borderRadius, 300);
|
||||
ctx.quadraticCurveTo(0, 300, 0, 300 - borderRadius);
|
||||
ctx.lineTo(0, borderRadius);
|
||||
ctx.quadraticCurveTo(0, 0, borderRadius, 0);
|
||||
ctx.closePath();
|
||||
ctx.clip();
|
||||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0, 300, 300);
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.png`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
});
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
img.src = url;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card hover>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-gray-900 mb-1">{qr.title}</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant={qr.type === 'DYNAMIC' ? 'info' : 'default'}>
|
||||
{qr.type}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dropdown
|
||||
align="right"
|
||||
trigger={
|
||||
<button className="p-1 hover:bg-gray-100 rounded">
|
||||
<svg className="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<DropdownItem onClick={() => downloadQR('png')}>Download PNG</DropdownItem>
|
||||
<DropdownItem onClick={() => downloadQR('svg')}>Download SVG</DropdownItem>
|
||||
{qr.type === 'DYNAMIC' && (
|
||||
<DropdownItem onClick={() => onEdit(qr.id)}>Edit</DropdownItem>
|
||||
)}
|
||||
<DropdownItem onClick={() => onDelete(qr.id)} className="text-red-600">
|
||||
Delete
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div id={`qr-${qr.id}`} className="flex items-center justify-center bg-gray-50 rounded-lg p-4 mb-3">
|
||||
<div className={qr.style?.cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}>
|
||||
<QRCodeSVG
|
||||
value={qrUrl}
|
||||
size={96}
|
||||
fgColor={qr.style?.foregroundColor || '#000000'}
|
||||
bgColor={qr.style?.backgroundColor || '#FFFFFF'}
|
||||
level="H"
|
||||
imageSettings={qr.style?.imageSettings ? {
|
||||
src: qr.style.imageSettings.src,
|
||||
height: qr.style.imageSettings.height * (96 / 200), // Scale logo for smaller QR
|
||||
width: qr.style.imageSettings.width * (96 / 200),
|
||||
excavate: qr.style.imageSettings.excavate,
|
||||
} : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-500">Type:</span>
|
||||
<span className="text-gray-900">{qr.contentType}</span>
|
||||
</div>
|
||||
{qr.type === 'DYNAMIC' && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-500">Scans:</span>
|
||||
<span className="text-gray-900">{qr.scans || 0}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-500">Created:</span>
|
||||
<span className="text-gray-900">{formatDate(qr.createdAt)}</span>
|
||||
</div>
|
||||
{qr.type === 'DYNAMIC' && (
|
||||
<div className="pt-2 border-t">
|
||||
<p className="text-xs text-gray-500">
|
||||
📊 Dynamic QR: Tracks scans via {baseUrl}/r/{qr.slug}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
@ -69,17 +69,17 @@ export const Pricing: React.FC<PricingProps> = ({ t }) => {
|
|||
{plan.key === 'free'
|
||||
? t.pricing[plan.key].price
|
||||
: billingPeriod === 'month'
|
||||
? t.pricing[plan.key].price
|
||||
: plan.key === 'pro'
|
||||
? '€90'
|
||||
: '€290'}
|
||||
? t.pricing[plan.key].price
|
||||
: plan.key === 'pro'
|
||||
? '€90'
|
||||
: '€290'}
|
||||
</span>
|
||||
<span className="text-gray-600 ml-2">
|
||||
{plan.key === 'free'
|
||||
? t.pricing[plan.key].period
|
||||
: billingPeriod === 'month'
|
||||
? t.pricing[plan.key].period
|
||||
: 'per year'}
|
||||
? t.pricing[plan.key].period
|
||||
: 'per year'}
|
||||
</span>
|
||||
</div>
|
||||
{billingPeriod === 'year' && plan.key !== 'free' && (
|
||||
|
|
@ -90,7 +90,7 @@ export const Pricing: React.FC<PricingProps> = ({ t }) => {
|
|||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<CardContent className="space-y-8">
|
||||
<ul className="space-y-3">
|
||||
{t.pricing[plan.key].features.map((feature: string, index: number) => (
|
||||
<li key={index} className="flex items-start space-x-3">
|
||||
|
|
@ -102,15 +102,17 @@ export const Pricing: React.FC<PricingProps> = ({ t }) => {
|
|||
))}
|
||||
</ul>
|
||||
|
||||
<Link href="/signup">
|
||||
<Button
|
||||
variant={plan.popular ? 'primary' : 'outline'}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="mt-8">
|
||||
<Link href="/signup">
|
||||
<Button
|
||||
variant={plan.popular ? 'primary' : 'outline'}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@
|
|||
"50 dynamic QR codes",
|
||||
"Unlimited static QR codes",
|
||||
"Advanced analytics (scans, devices, locations)",
|
||||
"Custom branding (colors)"
|
||||
"Custom branding (colors & logos)"
|
||||
]
|
||||
},
|
||||
"business": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue