Enroll modal: days input instead of date; hide bundle if base course owned

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 16:13:15 +05:00
parent 8c9f1e0a55
commit a290b9495c
2 changed files with 33 additions and 12 deletions
+10 -1
View File
@@ -57,11 +57,20 @@ export default async function StudentDashboard() {
// Витрина: продаваемые курсы, которых у студента нет → заблюренные карточки
const enrolledSlugs = new Set(enrollments.map((e) => e.course.slug));
// Не показываем «Всё включено», если у студента уже есть обычная версия того же курса
const SUPPRESS_IF_OWNED: Record<string, string> = {
"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 (
<main className="max-w-5xl mx-auto px-6 py-10 w-full">
+23 -11
View File
@@ -18,7 +18,7 @@ export function QuickEnrollButton({ userId, userName }: Props) {
const [courses, setCourses] = useState<Course[]>([]);
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) {
)}
</div>
{/* Expiry date (optional) */}
{/* Срок доступа в днях */}
<div>
<label
className="block text-xs font-bold uppercase tracking-widest mb-1.5"
style={{ color: "var(--muted-foreground)" }}
>
Срок доступа (необязательно)
Срок доступа, дней
</label>
<input
type="date"
value={expiresAt}
onChange={(e) => 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)")}
/>
<p className="text-xs mt-1" style={{ color: "var(--muted-foreground)" }}>
Оставьте пустым для бессрочного доступа
{(() => {
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 или пусто — бессрочный доступ";
})()}
</p>
</div>