qrmaster.net
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user