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