Add admin command menu, mobile drawer and animated course progress

- cmdk 1.1.1: Cmd/Ctrl+K command menu in the admin panel (students,
  courses, lessons, admin sections) plus a "Search" button in the sidebar.
  New read-only endpoint GET /api/admin/search with the same admin role
  check as /api/admin/export-users; query length 2-100 chars.
- vaul 1.1.2: ResponsiveDialog wrapper (Base UI Dialog on desktop, bottom
  drawer below 768px) styled per DS-2. Used for the storefront checkout on
  the dashboard and for course navigation on phones (tablet and desktop
  unchanged).
- @number-flow/react 0.6.2: animated "N of M lessons" and completion
  percent in course navigation; respects prefers-reduced-motion.

No migrations. Not deployed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-11 11:25:52 +05:00
co-authored by Claude Opus 5.5
parent 74d51f72bf
commit 09b069cbb5
10 changed files with 1469 additions and 186 deletions
+61
View File
@@ -0,0 +1,61 @@
import { NextRequest, NextResponse } from "next/server";
import { headers } from "next/headers";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
// Read-only lookup for the admin command menu (Ctrl/⌘+K).
const MIN_QUERY = 2;
const MAX_QUERY = 100;
export async function GET(request: NextRequest) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session || session.user.role !== "admin") {
return NextResponse.json({ error: "Нет доступа" }, { status: 403 });
}
const q = (request.nextUrl.searchParams.get("q") ?? "").trim().slice(0, MAX_QUERY);
if (q.length < MIN_QUERY) {
return NextResponse.json({ users: [], courses: [], lessons: [] });
}
const contains = { contains: q, mode: "insensitive" as const };
const [users, courses, lessons] = await Promise.all([
prisma.user.findMany({
where: { OR: [{ name: contains }, { email: contains }] },
select: { id: true, name: true, email: true, role: true },
orderBy: { createdAt: "desc" },
take: 8,
}),
prisma.course.findMany({
where: { OR: [{ title: contains }, { slug: contains }] },
select: { id: true, title: true, slug: true, published: true },
orderBy: { order: "asc" },
take: 6,
}),
prisma.lesson.findMany({
where: { title: contains },
select: {
id: true,
title: true,
published: true,
module: { select: { id: true, courseId: true, course: { select: { title: true } } } },
},
orderBy: { updatedAt: "desc" },
take: 8,
}),
]);
return NextResponse.json({
users,
courses,
lessons: lessons.map((l) => ({
id: l.id,
title: l.title,
published: l.published,
moduleId: l.module.id,
courseId: l.module.courseId,
courseTitle: l.module.course.title,
})),
});
}