Fehler....
This commit is contained in:
parent
e9bc1fe98b
commit
65def796ea
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue