Copy overhaul + qr designs
This commit is contained in:
@@ -4,6 +4,7 @@ import { db } from '@/lib/db';
|
||||
import { z } from 'zod';
|
||||
import { csrfProtection } from '@/lib/csrf';
|
||||
import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
|
||||
import { DYNAMIC_QR_LIMITS } from '@/lib/plans';
|
||||
|
||||
const updateQRSchema = z.object({
|
||||
title: z.string().min(1).optional(),
|
||||
@@ -103,6 +104,39 @@ export async function PATCH(
|
||||
return NextResponse.json({ error: 'QR code not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Reactivating a paused code consumes a slot again. Without this check,
|
||||
// pause -> create a new one -> unpause would quietly put the user over
|
||||
// their plan limit.
|
||||
if (
|
||||
data.status === 'ACTIVE' &&
|
||||
existing.status === 'PAUSED' &&
|
||||
existing.type === 'DYNAMIC'
|
||||
) {
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { plan: true },
|
||||
});
|
||||
const limit =
|
||||
DYNAMIC_QR_LIMITS[(user?.plan ?? 'FREE') as keyof typeof DYNAMIC_QR_LIMITS] ??
|
||||
DYNAMIC_QR_LIMITS.FREE;
|
||||
const activeCount = await db.qRCode.count({
|
||||
where: { userId, type: 'DYNAMIC', status: 'ACTIVE' },
|
||||
});
|
||||
|
||||
if (activeCount >= limit) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Limit reached',
|
||||
message: `You have ${activeCount} of ${limit} dynamic QR codes active. Pause another one first, or upgrade to reactivate this code.`,
|
||||
currentCount: activeCount,
|
||||
limit,
|
||||
plan: user?.plan ?? 'FREE',
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Static QR codes cannot be edited
|
||||
if (existing.type === 'STATIC' && data.content) {
|
||||
return NextResponse.json(
|
||||
@@ -119,6 +153,7 @@ export async function PATCH(
|
||||
...(data.content && { content: data.content }),
|
||||
...(data.tags && { tags: data.tags }),
|
||||
...(data.style && { style: data.style }),
|
||||
...(data.status && { status: data.status }),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const qrCodes = await db.qRCode.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
@@ -30,11 +32,26 @@ export async function GET(request: NextRequest) {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
// Recent activity per code. Used by the upgrade modal so someone deciding
|
||||
// which code to pause can see which one is actually dead, rather than
|
||||
// guessing from a lifetime total that says nothing about right now.
|
||||
const recentScans = await db.qRScan.groupBy({
|
||||
by: ['qrId'],
|
||||
where: {
|
||||
ts: { gte: thirtyDaysAgo },
|
||||
qr: { userId },
|
||||
},
|
||||
_count: { _all: true },
|
||||
});
|
||||
|
||||
const recentByQr = new Map(recentScans.map(r => [r.qrId, r._count._all]));
|
||||
|
||||
// Transform the data
|
||||
const transformed = qrCodes.map(qr => ({
|
||||
...qr,
|
||||
scans: qr._count.scans,
|
||||
uniqueScans: qr.scans.length, // Count of scans where isUnique=true
|
||||
scans30d: recentByQr.get(qr.id) ?? 0,
|
||||
_count: undefined,
|
||||
}));
|
||||
|
||||
@@ -113,10 +130,13 @@ export async function POST(request: NextRequest) {
|
||||
// Only check limits for DYNAMIC QR codes (static QR codes are unlimited)
|
||||
if (!isStatic) {
|
||||
// Count existing dynamic QR codes
|
||||
// Only ACTIVE codes consume a slot. Pausing a code frees one, which is what
|
||||
// the pricing page has always promised ("3 active dynamic QR codes").
|
||||
const dynamicQRCount = await db.qRCode.count({
|
||||
where: {
|
||||
userId,
|
||||
type: 'DYNAMIC',
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user