Fehler....

This commit is contained in:
knuthtimo-lab 2026-01-13 17:08:06 +01:00
parent e9bc1fe98b
commit 65def796ea
2 changed files with 25 additions and 273 deletions

View File

@ -1,271 +0,0 @@
'use client';
import React, { useState, useEffect } from 'react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
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';
// Note: Metadata is defined in a separate metadata.ts file for client components
// or the parent layout should be updated to allow indexing for this specific page.
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
const fetchUserPlan = async () => {
try {
const response = await fetch('/api/user/plan');
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);
}
};
fetchUserPlan();
}, []);
const handleUpgrade = async (plan: 'PRO' | 'BUSINESS') => {
setLoading(plan);
try {
const response = await fetch('/api/stripe/create-checkout-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
plan,
billingInterval: billingPeriod === 'month' ? 'month' : 'year',
}),
});
if (!response.ok) {
throw new Error('Failed to create checkout session');
}
const { url } = await response.json();
window.location.href = url;
} catch (error) {
console.error('Error creating checkout session:', error);
showToast('Failed to start checkout. Please try again.', 'error');
setLoading(null);
}
};
const handleDowngrade = async () => {
// Show confirmation dialog
const confirmed = window.confirm(
'Are you sure you want to downgrade to the Free plan? Your subscription will be canceled immediately and you will lose access to premium features.'
);
if (!confirmed) {
return;
}
setLoading('FREE');
try {
const response = await fetch('/api/stripe/cancel-subscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to cancel subscription');
}
showToast('Successfully downgraded to Free plan', 'success');
// Refresh to update the plan
setTimeout(() => {
window.location.reload();
}, 1500);
} catch (error: any) {
console.error('Error canceling subscription:', error);
showToast(error.message || 'Failed to downgrade. Please try again.', 'error');
setLoading(null);
}
};
// 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,
disabled: currentPlan === 'FREE',
popular: false,
onDowngrade: handleDowngrade,
},
{
key: 'pro',
name: 'Pro',
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 & logos)',
],
buttonText: isCurrentPlanWithInterval('PRO', selectedInterval)
? 'Current Plan'
: hasPlanDifferentInterval('PRO')
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
: 'Upgrade to Pro',
buttonVariant: 'primary' as const,
disabled: isCurrentPlanWithInterval('PRO', selectedInterval),
popular: true,
onUpgrade: () => handleUpgrade('PRO'),
},
{
key: 'business',
name: 'Business',
price: billingPeriod === 'month' ? '€29' : '€290',
period: billingPeriod === 'month' ? 'per month' : 'per year',
showDiscount: billingPeriod === 'year',
features: [
'500 dynamic QR codes',
'Unlimited static QR codes',
'Everything from Pro',
'Bulk QR Creation (up to 1,000)',
'Priority email support',
'Advanced tracking & insights',
],
buttonText: isCurrentPlanWithInterval('BUSINESS', selectedInterval)
? 'Current Plan'
: hasPlanDifferentInterval('BUSINESS')
? `Switch to ${billingPeriod === 'month' ? 'Monthly' : 'Yearly'}`
: 'Upgrade to Business',
buttonVariant: 'primary' as const,
disabled: isCurrentPlanWithInterval('BUSINESS', selectedInterval),
popular: false,
onUpgrade: () => handleUpgrade('BUSINESS'),
},
];
return (
<div className="container mx-auto px-4 py-12">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Choose Your Plan
</h1>
<p className="text-xl text-gray-600">
Select the perfect plan for your QR code needs
</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
key={plan.key}
className={plan.popular ? 'border-primary-500 shadow-xl relative' : ''}
>
{plan.popular && (
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
<Badge variant="info" className="px-3 py-1">
Most Popular
</Badge>
</div>
)}
<CardHeader className="text-center pb-8">
<CardTitle className="text-2xl mb-4">
{plan.name}
</CardTitle>
<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>
<CardContent className="space-y-6">
<ul className="space-y-3">
{plan.features.map((feature: string, index: number) => (
<li key={index} className="flex items-start space-x-3">
<svg className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
<span className="text-gray-700">{feature}</span>
</li>
))}
</ul>
<Button
variant={plan.buttonVariant}
className="w-full"
size="lg"
disabled={plan.disabled || loading === plan.key.toUpperCase()}
onClick={plan.key === 'free' ? (plan as any).onDowngrade : (plan as any).onUpgrade}
>
{loading === plan.key.toUpperCase() ? 'Processing...' : plan.buttonText}
</Button>
</CardContent>
</Card>
))}
</div>
<div className="text-center mt-12">
<p className="text-gray-600">
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.net" className="text-primary-600 hover:text-primary-700 underline">Contact our team</a>
</p>
</div>
</div>
);
}

View File

@ -1,9 +1,9 @@
'use client'; 'use client';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, Suspense } from 'react';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
export default function VCardPage() { function VCardContent() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [firstName, setFirstName] = useState(''); const [firstName, setFirstName] = useState('');
@ -272,3 +272,26 @@ END:VCARD`;
</div> </div>
); );
} }
export default function VCardPage() {
return (
<Suspense fallback={
<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...</p>
</div>
</div>
}>
<VCardContent />
</Suspense>
);
}