From a290b9495c7963b9d9a1f1b65ba5fd526204a258 Mon Sep 17 00:00:00 2001 From: dmitriylaukhin Date: Mon, 8 Jun 2026 16:13:15 +0500 Subject: [PATCH] Enroll modal: days input instead of date; hide bundle if base course owned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Quick-enroll modal now takes access duration in days (0/empty = unlimited) and shows the computed expiry date, instead of a manual date picker. - Dashboard upsell no longer shows the "Всё включено" bundle of a course the student already owns in its base edition (obsidian/zotero). Co-Authored-By: Claude Opus 4.8 --- src/app/(student)/dashboard/page.tsx | 11 ++++++- src/components/admin/quick-enroll-modal.tsx | 34 ++++++++++++++------- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/app/(student)/dashboard/page.tsx b/src/app/(student)/dashboard/page.tsx index ec6cb12..61737c4 100644 --- a/src/app/(student)/dashboard/page.tsx +++ b/src/app/(student)/dashboard/page.tsx @@ -57,11 +57,20 @@ export default async function StudentDashboard() { // Витрина: продаваемые курсы, которых у студента нет → заблюренные карточки const enrolledSlugs = new Set(enrollments.map((e) => e.course.slug)); + // Не показываем «Всё включено», если у студента уже есть обычная версия того же курса + const SUPPRESS_IF_OWNED: Record = { + "obsidian-full": "obsidian", // ВВ Obsidian 2025 ← обычный Obsidian 2025 + "zotero-full": "zotero", // ВВ Zotero ← обычный Zotero + }; const storeCourses = await prisma.course.findMany({ where: { slug: { in: Object.keys(STORE_LINKS) }, published: true }, select: { id: true, slug: true, title: true, description: true, coverImage: true }, }); - const locked = storeCourses.filter((c) => !enrolledSlugs.has(c.slug)); + const locked = storeCourses.filter( + (c) => + !enrolledSlugs.has(c.slug) && + !(SUPPRESS_IF_OWNED[c.slug] && enrolledSlugs.has(SUPPRESS_IF_OWNED[c.slug])) + ); return (
diff --git a/src/components/admin/quick-enroll-modal.tsx b/src/components/admin/quick-enroll-modal.tsx index 356d170..7177d28 100644 --- a/src/components/admin/quick-enroll-modal.tsx +++ b/src/components/admin/quick-enroll-modal.tsx @@ -18,7 +18,7 @@ export function QuickEnrollButton({ userId, userName }: Props) { const [courses, setCourses] = useState([]); const [loadingCourses, setLoadingCourses] = useState(false); const [courseId, setCourseId] = useState(""); - const [expiresAt, setExpiresAt] = useState(""); + const [days, setDays] = useState(""); const [status, setStatus] = useState<"idle" | "success" | "error">("idle"); const [errorMsg, setErrorMsg] = useState(""); const [pending, startTransition] = useTransition(); @@ -49,7 +49,7 @@ export function QuickEnrollButton({ userId, userName }: Props) { function handleOpen() { setStatus("idle"); setErrorMsg(""); - setExpiresAt(""); + setDays(""); setCourseId(""); setOpen(true); } @@ -61,7 +61,9 @@ export function QuickEnrollButton({ userId, userName }: Props) { function handleSubmit() { if (!courseId) return; - const expiry = expiresAt ? new Date(expiresAt) : null; + // Количество дней доступа: 0 или пусто → бессрочно (null) + const d = parseInt(days, 10); + const expiry = !isNaN(d) && d > 0 ? new Date(Date.now() + d * 86400000) : null; startTransition(async () => { const result = await grantCourseAccess(userId, courseId, expiry); if ("error" in result) { @@ -178,25 +180,28 @@ export function QuickEnrollButton({ userId, userName }: Props) { )} - {/* Expiry date (optional) */} + {/* Срок доступа в днях */}
setExpiresAt(e.target.value)} + type="number" + min={0} + step={1} + inputMode="numeric" + value={days} + onChange={(e) => setDays(e.target.value)} disabled={pending} - min={new Date().toISOString().slice(0, 10)} + placeholder="0" style={{ width: "100%", border: "2px solid var(--border)", background: "var(--background)", - color: expiresAt ? "var(--foreground)" : "var(--muted-foreground)", + color: days ? "var(--foreground)" : "var(--muted-foreground)", padding: "0.4rem 0.5rem", fontSize: "13px", fontFamily: "inherit", @@ -206,7 +211,14 @@ export function QuickEnrollButton({ userId, userName }: Props) { onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")} />

- Оставьте пустым для бессрочного доступа + {(() => { + const d = parseInt(days, 10); + if (!isNaN(d) && d > 0) { + const until = new Date(Date.now() + d * 86400000).toLocaleDateString("ru-RU"); + return `Доступ до ${until} (${d} дн.)`; + } + return "0 или пусто — бессрочный доступ"; + })()}