fix: LOW priority — GPU hints, CSRF cleanup, redundant query removal, mobile perf
- Add will-change to .hero-glow-orb (filter, transform) and .team-card-glitter::before (background-position) - Clear CSRF cookie on logout alongside auth cookie - Add max array length (100) validation on team reorder endpoint - Remove redundant isOpenDayClassBookedByPhone pre-check (DB UNIQUE constraint handles it) - Extract Schedule grid layout calculation into useMemo - Reduce HeroLogo sparkle animations on mobile (15 → 8 via hidden sm:block) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,8 @@ import { revalidatePath } from "next/cache";
|
|||||||
export async function PUT(request: NextRequest) {
|
export async function PUT(request: NextRequest) {
|
||||||
const { ids } = await request.json() as { ids: number[] };
|
const { ids } = await request.json() as { ids: number[] };
|
||||||
|
|
||||||
if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => Number.isInteger(id) && id > 0)) {
|
if (!Array.isArray(ids) || ids.length === 0 || ids.length > 100 || !ids.every((id) => Number.isInteger(id) && id > 0)) {
|
||||||
return NextResponse.json({ error: "ids must be a non-empty array of positive integers" }, { status: 400 });
|
return NextResponse.json({ error: "ids must be a non-empty array of positive integers (max 100)" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
reorderTeamMembers(ids);
|
reorderTeamMembers(ids);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { COOKIE_NAME } from "@/lib/auth";
|
import { COOKIE_NAME, CSRF_COOKIE_NAME } from "@/lib/auth";
|
||||||
|
|
||||||
export async function POST() {
|
export async function POST() {
|
||||||
const response = NextResponse.json({ ok: true });
|
const response = NextResponse.json({ ok: true });
|
||||||
@@ -8,5 +8,9 @@ export async function POST() {
|
|||||||
path: "/",
|
path: "/",
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
});
|
});
|
||||||
|
response.cookies.set(CSRF_COOKIE_NAME, "", {
|
||||||
|
path: "/",
|
||||||
|
maxAge: 0,
|
||||||
|
});
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import {
|
import {
|
||||||
addOpenDayBooking,
|
addOpenDayBooking,
|
||||||
isOpenDayClassBookedByPhone,
|
|
||||||
getPersonOpenDayBookings,
|
getPersonOpenDayBookings,
|
||||||
getOpenDayEvent,
|
getOpenDayEvent,
|
||||||
} from "@/lib/db";
|
} from "@/lib/db";
|
||||||
@@ -35,11 +34,6 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: "Телефон обязателен" }, { status: 400 });
|
return NextResponse.json({ error: "Телефон обязателен" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check not already booked
|
|
||||||
if (isOpenDayClassBookedByPhone(classId, cleanPhone)) {
|
|
||||||
return NextResponse.json({ error: "Вы уже записаны на это занятие" }, { status: 409 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = addOpenDayBooking(classId, eventId, {
|
const id = addOpenDayBooking(classId, eventId, {
|
||||||
name: cleanName,
|
name: cleanName,
|
||||||
phone: cleanPhone,
|
phone: cleanPhone,
|
||||||
|
|||||||
@@ -128,6 +128,7 @@
|
|||||||
filter: blur(80px);
|
filter: blur(80px);
|
||||||
animation: pulse-glow 6s ease-in-out infinite;
|
animation: pulse-glow 6s ease-in-out infinite;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
will-change: filter, transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Gradient Text ===== */
|
/* ===== Gradient Text ===== */
|
||||||
@@ -322,6 +323,7 @@
|
|||||||
mask-composite: exclude;
|
mask-composite: exclude;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
will-change: background-position;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Notification Pulse ===== */
|
/* ===== Notification Pulse ===== */
|
||||||
|
|||||||
@@ -222,6 +222,20 @@ export function Schedule({ data: schedule, classItems }: ScheduleProps) {
|
|||||||
dispatch({ type: "SET_LOCATION", mode });
|
dispatch({ type: "SET_LOCATION", mode });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const gridLayout = useMemo(() => {
|
||||||
|
const len = filteredDays.length;
|
||||||
|
const cls = len >= 7 ? "sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-7"
|
||||||
|
: len >= 6 ? "sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6"
|
||||||
|
: len >= 4 ? "sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5"
|
||||||
|
: len === 3 ? "sm:grid-cols-2 lg:grid-cols-3"
|
||||||
|
: len === 2 ? "sm:grid-cols-2"
|
||||||
|
: "justify-items-center";
|
||||||
|
const style = len === 1 ? undefined
|
||||||
|
: len <= 3 && len > 0 ? { maxWidth: len * 340 + (len - 1) * 12, marginInline: "auto" as const }
|
||||||
|
: undefined;
|
||||||
|
return { cls, style };
|
||||||
|
}, [filteredDays.length]);
|
||||||
|
|
||||||
const activeTabClass = "bg-gold text-black shadow-[0_0_20px_rgba(201,169,110,0.3)]";
|
const activeTabClass = "bg-gold text-black shadow-[0_0_20px_rgba(201,169,110,0.3)]";
|
||||||
const inactiveTabClass = "border border-neutral-300 text-neutral-500 hover:border-neutral-400 hover:text-neutral-700 dark:border-white/10 dark:text-neutral-400 dark:hover:text-white dark:hover:border-white/20";
|
const inactiveTabClass = "border border-neutral-300 text-neutral-500 hover:border-neutral-400 hover:text-neutral-700 dark:border-white/10 dark:text-neutral-400 dark:hover:text-white dark:hover:border-white/20";
|
||||||
|
|
||||||
@@ -349,8 +363,8 @@ export function Schedule({ data: schedule, classItems }: ScheduleProps) {
|
|||||||
{/* Desktop: grid layout */}
|
{/* Desktop: grid layout */}
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<div
|
<div
|
||||||
className={`mt-8 hidden sm:grid grid-cols-1 gap-3 px-4 sm:px-6 lg:px-8 xl:px-6 ${filteredDays.length >= 7 ? "sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-7" : filteredDays.length >= 6 ? "sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6" : filteredDays.length >= 4 ? "sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5" : filteredDays.length === 3 ? "sm:grid-cols-2 lg:grid-cols-3" : filteredDays.length === 2 ? "sm:grid-cols-2" : "justify-items-center"}`}
|
className={`mt-8 hidden sm:grid grid-cols-1 gap-3 px-4 sm:px-6 lg:px-8 xl:px-6 ${gridLayout.cls}`}
|
||||||
style={filteredDays.length === 1 ? undefined : filteredDays.length <= 3 && filteredDays.length > 0 ? { maxWidth: filteredDays.length * 340 + (filteredDays.length - 1) * 12, marginInline: "auto" } : undefined}
|
style={gridLayout.style}
|
||||||
>
|
>
|
||||||
{filteredDays.map((day) => (
|
{filteredDays.map((day) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -96,10 +96,10 @@ export function HeroLogo({ className = "", size = 220 }: HeroLogoProps) {
|
|||||||
d={FULL_PATH}
|
d={FULL_PATH}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Glitter sparkles on heart surface */}
|
{/* Glitter sparkles on heart surface — odd-indexed hidden on mobile via CSS class */}
|
||||||
<g clipPath="url(#heart-clip)" filter="url(#sparkle-glow)">
|
<g clipPath="url(#heart-clip)" filter="url(#sparkle-glow)">
|
||||||
{SPARKLES.map((s, i) => (
|
{SPARKLES.map((s, i) => (
|
||||||
<circle key={`sparkle-${i}`} cx={s.x} cy={s.y} r="1.8" fill="#d4b87a">
|
<circle key={`sparkle-${i}`} cx={s.x} cy={s.y} r="1.8" fill="#d4b87a" className={i % 2 ? "hidden sm:block" : ""}>
|
||||||
<animate
|
<animate
|
||||||
attributeName="opacity"
|
attributeName="opacity"
|
||||||
values="0;0;0.9;1;0.9;0;0"
|
values="0;0;0.9;1;0.9;0;0"
|
||||||
|
|||||||
Reference in New Issue
Block a user