Copy overhaul + qr designs

This commit is contained in:
2026-07-27 17:54:59 +02:00
parent 033bc7e29d
commit 70d97aa970
144 changed files with 23107 additions and 1699 deletions

View File

@@ -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 }),
},
});

View File

@@ -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',
},
});