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:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user