qrmaster.net

This commit is contained in:
Timo Knuth
2025-12-09 22:22:36 +01:00
parent 424c61a176
commit 8c5e2fa58e
37 changed files with 549 additions and 915 deletions

View File

@@ -344,7 +344,13 @@ export default function AnalyticsPage() {
<td className="px-4 py-4 align-middle">{country.count.toLocaleString()}</td>
<td className="px-4 py-4 align-middle">{country.percentage}%</td>
<td className="px-4 py-4 align-middle">
<Badge variant="success"></Badge>
<Badge variant={
country.trend === 'up' ? 'success' :
country.trend === 'down' ? 'destructive' :
'default'
}>
{country.trend === 'up' ? '↑' : country.trend === 'down' ? '↓' : '→'} {country.trendPercentage}%
</Badge>
</td>
</tr>
))}
@@ -387,8 +393,12 @@ export default function AnalyticsPage() {
<td className="px-4 py-4 align-middle">{qr.uniqueScans.toLocaleString()}</td>
<td className="px-4 py-4 align-middle">{qr.conversion}%</td>
<td className="px-4 py-4 align-middle">
<Badge variant={qr.totalScans > 0 ? 'success' : 'default'}>
{qr.totalScans > 0 ? '↑' : '—'}
<Badge variant={
qr.trend === 'up' ? 'success' :
qr.trend === 'down' ? 'destructive' :
'default'
}>
{qr.trend === 'up' ? '↑' : qr.trend === 'down' ? '↓' : '→'} {qr.trendPercentage}%
</Badge>
</td>
</tr>

View File

@@ -20,7 +20,6 @@ interface QRCodeData {
contentType: string;
content?: any;
slug: string;
status: 'ACTIVE' | 'PAUSED';
createdAt: string;
scans: number;
style?: any;
@@ -219,36 +218,6 @@ export default function DashboardPage() {
router.push(`/qr/${id}/edit`);
};
const handlePause = async (id: string) => {
try {
const qr = qrCodes.find(q => q.id === id);
if (!qr) return;
const newStatus = qr.status === 'ACTIVE' ? 'PAUSED' : 'ACTIVE';
const response = await fetch(`/api/qrs/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ status: newStatus }),
});
if (response.ok) {
// Update local state
setQrCodes(qrCodes.map(q =>
q.id === id ? { ...q, status: newStatus } : q
));
showToast(`QR code ${newStatus === 'ACTIVE' ? 'resumed' : 'paused'}!`, 'success');
} else {
throw new Error('Failed to update status');
}
} catch (error) {
console.error('Error updating QR status:', error);
showToast('Failed to update QR code status', 'error');
}
};
const handleDelete = async (id: string) => {
if (!confirm('Are you sure you want to delete this QR code? This action cannot be undone.')) {
return;
@@ -393,7 +362,6 @@ export default function DashboardPage() {
key={qr.id}
qr={qr}
onEdit={handleEdit}
onPause={handlePause}
onDelete={handleDelete}
/>
))}

View File

@@ -6,11 +6,14 @@ import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { showToast } from '@/components/ui/Toast';
import { useRouter } from 'next/navigation';
import { BillingToggle } from '@/components/ui/BillingToggle';
export default function PricingPage() {
const router = useRouter();
const [loading, setLoading] = useState<string | null>(null);
const [currentPlan, setCurrentPlan] = useState<string>('FREE');
const [currentInterval, setCurrentInterval] = useState<'month' | 'year' | null>(null);
const [billingPeriod, setBillingPeriod] = useState<'month' | 'year'>('month');
useEffect(() => {
// Fetch current user plan
@@ -20,6 +23,7 @@ export default function PricingPage() {
if (response.ok) {
const data = await response.json();
setCurrentPlan(data.plan || 'FREE');
setCurrentInterval(data.interval || null);
}
} catch (error) {
console.error('Error fetching user plan:', error);
@@ -40,7 +44,7 @@ export default function PricingPage() {
},
body: JSON.stringify({
plan,
billingInterval: 'month',
billingInterval: billingPeriod === 'month' ? 'month' : 'year',
}),
});
@@ -95,17 +99,31 @@ export default function PricingPage() {
}
};
// Helper function to check if this is the user's exact current plan (plan + interval)
const isCurrentPlanWithInterval = (planType: string, interval: 'month' | 'year') => {
return currentPlan === planType && currentInterval === interval;
};
// Helper function to check if user has this plan but different interval
const hasPlanDifferentInterval = (planType: string) => {
return currentPlan === planType && currentInterval && currentInterval !== billingPeriod;
};
const selectedInterval = billingPeriod === 'month' ? 'month' : 'year';
const plans = [
{
key: 'free',
name: 'Free',
price: '€0',
period: 'forever',
showDiscount: false,
features: [
'3 dynamic QR codes',
'Unlimited static QR codes',
'Basic scan tracking',
'Standard QR design templates',
'Download as SVG/PNG',
],
buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Downgrade to Free',
buttonVariant: 'outline' as const,
@@ -116,26 +134,31 @@ export default function PricingPage() {
{
key: 'pro',
name: 'Pro',
price: '€9',
period: 'per month',
price: billingPeriod === 'month' ? '€9' : '€90',
period: billingPeriod === 'month' ? 'per month' : 'per year',
showDiscount: billingPeriod === 'year',
features: [
'50 dynamic QR codes',
'Unlimited static QR codes',
'Advanced analytics (scans, devices, locations)',
'Custom branding (colors)',
'Download as SVG/PNG',
],
buttonText: currentPlan === 'PRO' ? 'Current Plan' : 'Upgrade to Pro',
buttonText: isCurrentPlanWithInterval('PRO', selectedInterval)
? 'Current Plan'
: hasPlanDifferentInterval('PRO')
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
: 'Upgrade to Pro',
buttonVariant: 'primary' as const,
disabled: currentPlan === 'PRO',
disabled: isCurrentPlanWithInterval('PRO', selectedInterval),
popular: true,
onUpgrade: () => handleUpgrade('PRO'),
},
{
key: 'business',
name: 'Business',
price: '€29',
period: 'per month',
price: billingPeriod === 'month' ? '€29' : '€290',
period: billingPeriod === 'month' ? 'per month' : 'per year',
showDiscount: billingPeriod === 'year',
features: [
'500 dynamic QR codes',
'Unlimited static QR codes',
@@ -144,9 +167,13 @@ export default function PricingPage() {
'Priority email support',
'Advanced tracking & insights',
],
buttonText: currentPlan === 'BUSINESS' ? 'Current Plan' : 'Upgrade to Business',
buttonText: isCurrentPlanWithInterval('BUSINESS', selectedInterval)
? 'Current Plan'
: hasPlanDifferentInterval('BUSINESS')
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
: 'Upgrade to Business',
buttonVariant: 'primary' as const,
disabled: currentPlan === 'BUSINESS',
disabled: isCurrentPlanWithInterval('BUSINESS', selectedInterval),
popular: false,
onUpgrade: () => handleUpgrade('BUSINESS'),
},
@@ -163,6 +190,10 @@ export default function PricingPage() {
</p>
</div>
<div className="flex justify-center mb-8">
<BillingToggle value={billingPeriod} onChange={setBillingPeriod} />
</div>
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto">
{plans.map((plan) => (
<Card
@@ -181,13 +212,20 @@ export default function PricingPage() {
<CardTitle className="text-2xl mb-4">
{plan.name}
</CardTitle>
<div className="flex items-baseline justify-center">
<span className="text-4xl font-bold">
{plan.price}
</span>
<span className="text-gray-600 ml-2">
{plan.period}
</span>
<div className="flex flex-col items-center">
<div className="flex items-baseline justify-center">
<span className="text-4xl font-bold">
{plan.price}
</span>
<span className="text-gray-600 ml-2">
{plan.period}
</span>
</div>
{plan.showDiscount && (
<Badge variant="success" className="mt-2">
Save 16%
</Badge>
)}
</div>
</CardHeader>
@@ -222,7 +260,7 @@ export default function PricingPage() {
All plans include unlimited static QR codes and basic customization.
</p>
<p className="text-gray-600 mt-2">
Need help choosing? <a href="mailto:support@qrmaster.com" className="text-primary-600 hover:text-primary-700 underline">Contact our team</a>
Need help choosing? <a href="mailto:support@qrmaster.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</a>
</p>
</div>
</div>