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 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({
|
const storeCourses = await prisma.course.findMany({
|
||||||
where: { slug: { in: Object.keys(STORE_LINKS) }, published: true },
|
where: { slug: { in: Object.keys(STORE_LINKS) }, published: true },
|
||||||
select: { id: true, slug: true, title: true, description: true, coverImage: 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 (
|
return (
|
||||||
<main className="max-w-5xl mx-auto px-6 py-10 w-full">
|
<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 [courses, setCourses] = useState<Course[]>([]);
|
||||||
const [loadingCourses, setLoadingCourses] = useState(false);
|
const [loadingCourses, setLoadingCourses] = useState(false);
|
||||||
const [courseId, setCourseId] = useState("");
|
const [courseId, setCourseId] = useState("");
|
||||||
const [expiresAt, setExpiresAt] = useState("");
|
const [days, setDays] = useState("");
|
||||||
const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
|
const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
|
||||||
const [errorMsg, setErrorMsg] = useState("");
|
const [errorMsg, setErrorMsg] = useState("");
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
@@ -49,7 +49,7 @@ export function QuickEnrollButton({ userId, userName }: Props) {
|
|||||||
function handleOpen() {
|
function handleOpen() {
|
||||||
setStatus("idle");
|
setStatus("idle");
|
||||||
setErrorMsg("");
|
setErrorMsg("");
|
||||||
setExpiresAt("");
|
setDays("");
|
||||||
setCourseId("");
|
setCourseId("");
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,9 @@ export function QuickEnrollButton({ userId, userName }: Props) {
|
|||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
if (!courseId) return;
|
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 () => {
|
startTransition(async () => {
|
||||||
const result = await grantCourseAccess(userId, courseId, expiry);
|
const result = await grantCourseAccess(userId, courseId, expiry);
|
||||||
if ("error" in result) {
|
if ("error" in result) {
|
||||||
@@ -178,25 +180,28 @@ export function QuickEnrollButton({ userId, userName }: Props) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expiry date (optional) */}
|
{/* Срок доступа в днях */}
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
className="block text-xs font-bold uppercase tracking-widest mb-1.5"
|
className="block text-xs font-bold uppercase tracking-widest mb-1.5"
|
||||||
style={{ color: "var(--muted-foreground)" }}
|
style={{ color: "var(--muted-foreground)" }}
|
||||||
>
|
>
|
||||||
Срок доступа (необязательно)
|
Срок доступа, дней
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="number"
|
||||||
value={expiresAt}
|
min={0}
|
||||||
onChange={(e) => setExpiresAt(e.target.value)}
|
step={1}
|
||||||
|
inputMode="numeric"
|
||||||
|
value={days}
|
||||||
|
onChange={(e) => setDays(e.target.value)}
|
||||||
disabled={pending}
|
disabled={pending}
|
||||||
min={new Date().toISOString().slice(0, 10)}
|
placeholder="0"
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
border: "2px solid var(--border)",
|
border: "2px solid var(--border)",
|
||||||
background: "var(--background)",
|
background: "var(--background)",
|
||||||
color: expiresAt ? "var(--foreground)" : "var(--muted-foreground)",
|
color: days ? "var(--foreground)" : "var(--muted-foreground)",
|
||||||
padding: "0.4rem 0.5rem",
|
padding: "0.4rem 0.5rem",
|
||||||
fontSize: "13px",
|
fontSize: "13px",
|
||||||
fontFamily: "inherit",
|
fontFamily: "inherit",
|
||||||
@@ -206,7 +211,14 @@ export function QuickEnrollButton({ userId, userName }: Props) {
|
|||||||
onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")}
|
onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs mt-1" style={{ color: "var(--muted-foreground)" }}>
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user