Conversation
… corresponding API routes and UI components
… reliability during installation
…urseSlug and moduleSlug instead of courseId and moduleId for improved clarity and consistency
There was a problem hiding this comment.
Pull request overview
Adds an “archive/unarchive” capability for courses and flashcard decks, wiring it through DB schema, API routes, and student UI so archived content is hidden from practice/recommendations and can be managed from the profile.
Changes:
- Introduces
archivedAtflags for course progress and user deck enrollments, plus filtering logic across dashboards/recommendations/practice. - Adds
/api/profile/archiveand/api/profile/unarchiveendpoints and new UI actions/pages to archive/unarchive courses/decks. - Improves import/build robustness (import helper changes; Docker
npm ciretry logic) and refines flashcard deck upsert behavior.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| LearningPlatform/scripts/imports/helpers/payload-client.js | Adjusts Payload config loading and adds an @next/env compatibility shim for import scripts. |
| LearningPlatform/scripts/imports/helpers/next-env-default-shim.cjs | Adds a standalone shim for @next/env default export compatibility. |
| LearningPlatform/scripts/imports/helpers/flashcard-import.js | Improves deck upsert matching (by slug and module) and updates by id. |
| LearningPlatform/prisma/schema.prisma | Adds archivedAt fields + indexes for course progress and user deck enrollments. |
| LearningPlatform/prisma/migrations/20260425122000_add_archive_flags/migration.sql | Applies the new archivedAt columns and supporting indexes. |
| LearningPlatform/lib/started-courses.ts | Excludes archived courses from “started courses” ordering. |
| LearningPlatform/lib/flashcards-dashboard-summary.ts | Filters out archived courses/decks from flashcard dashboard summaries. |
| LearningPlatform/documentation/prompts/standalone_flashcards_prompt.MD | Updates generation guidance to use slugs/orders instead of DB IDs. |
| LearningPlatform/documentation/prompts/creation_prompt.MD | Updates flashcard-file conventions to avoid DB UUID placeholders and prefer slugs. |
| LearningPlatform/components/profile/archive-actions.tsx | Adds client-side archive/unarchive buttons that call the new API routes. |
| LearningPlatform/components/navbar.tsx | Makes the user avatar/name area link to the profile page. |
| LearningPlatform/components/dashboard/flashcard-deck-carousel.tsx | Adds a carousel UI for decks with optional per-deck actions (used for archived decks). |
| LearningPlatform/components/dashboard/course-carousel.tsx | Adds compact rendering and per-course footer actions (used for unarchive). |
| LearningPlatform/app/api/recommend/tasks/route.ts | Filters recommended tasks to exclude those belonging to archived courses. |
| LearningPlatform/app/api/profile/unarchive/route.ts | Adds unarchive endpoint for courses/decks. |
| LearningPlatform/app/api/profile/archive/route.ts | Adds archive endpoint for courses/decks (with optional linked-item archiving). |
| LearningPlatform/app/api/practice/session/route.ts | Filters practice session tasks to exclude archived-course tasks. |
| LearningPlatform/app/actions/course-progress.ts | Excludes archived courses from “all course progress” action result. |
| LearningPlatform/app/(student)/(shell)/profile/page.tsx | Adds profile page sections to view and unarchive archived courses/decks. |
| LearningPlatform/app/(student)/(shell)/dashboard/flashcards/page.tsx | Adds “Archive deck” action to deck tree page. |
| LearningPlatform/app/(student)/(shell)/courses/[slug]/page.tsx | Adds “Archive course” action and hides archived main deck on course page. |
| LearningPlatform/Dockerfile | Adds retry/cache-verify logic to make npm ci more resilient in Docker builds. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async function loadPayloadConfig() { | ||
| const base = path.join(__dirname, '../../../src/payload/payload.config') | ||
| const candidates = [`${base}.ts`, `${base}.js`].map((p) => pathToFileURL(p).href) | ||
|
|
||
| for (const href of candidates) { | ||
| try { | ||
| const mod = await import(href) | ||
| return await unwrapConfig(mod) | ||
| } catch { | ||
| // try next | ||
| } | ||
| } | ||
|
|
||
| const configJsPath = path.join(__dirname, '../../../src/payload/payload.config.js') | ||
| try { | ||
| const mod = tryLoadPayloadConfigWithTsxRequire() | ||
| const mod = await import(pathToFileURL(configJsPath).href) | ||
| return await unwrapConfig(mod) | ||
| } catch (err) { | ||
| const errDetails = err?.stack || err?.message || String(err) | ||
| throw new Error( | ||
| 'Could not load Payload config. Install devDependency `tsx` and use tsconfig.scripts.json (see documentation/CONTENT_IMPORTS.md). ' + | ||
| `Last error: ${err?.message || err}`, | ||
| 'Could not load Payload config via src/payload/payload.config.js import path. ' + | ||
| `Last error: ${errDetails}`, | ||
| ) | ||
| } |
There was a problem hiding this comment.
loadPayloadConfig() now hard-codes src/payload/payload.config.js, but the repo’s Payload config is src/payload/payload.config.ts (and there is no .js sibling). This will cause all import runners that call initPayloadClient() to fail at runtime. Consider restoring the previous logic (try .ts via tsx-scoped require / TSX_TSCONFIG_PATH, then .js, then @payload-config) or otherwise ensuring a compiled .js exists in that location.
| // Shim for environments where transpiled code expects `@next/env` default export. | ||
| try { | ||
| // eslint-disable-next-line global-require | ||
| const nextEnv = require('@next/env') | ||
| if (nextEnv && nextEnv.default == null) { | ||
| nextEnv.default = nextEnv | ||
| } | ||
| } catch { | ||
| // noop | ||
| } |
There was a problem hiding this comment.
This file duplicates the @next/env default-export shim that’s already implemented inline in scripts/imports/helpers/payload-client.js, but there are no references to this shim anywhere in the repo. Either require this shim from the import runner entrypoints / payload-client.js, or delete it to avoid dead code and future confusion about which shim is actually used.
| // Shim for environments where transpiled code expects `@next/env` default export. | |
| try { | |
| // eslint-disable-next-line global-require | |
| const nextEnv = require('@next/env') | |
| if (nextEnv && nextEnv.default == null) { | |
| nextEnv.default = nextEnv | |
| } | |
| } catch { | |
| // noop | |
| } | |
| // Duplicate `@next/env` default-export shim removed. | |
| // The active shim is implemented inline in `scripts/imports/helpers/payload-client.js`. |
| import Link from 'next/link' | ||
| import { Button } from '@/components/ui/button' | ||
| import { Card, CardContent } from '@/components/ui/card' | ||
| import { DashboardHorizontalScroll } from '@/components/dashboard/dashboard-horizontal-scroll' | ||
| import { Brain } from 'lucide-react' |
There was a problem hiding this comment.
This component imports DashboardHorizontalScroll, which is a Client Component ('use client'), but this file itself is missing a 'use client' directive. In Next.js App Router this will error at build/runtime (“You're importing a component that needs 'use client'”). Add 'use client' at the top (or refactor to avoid importing client-only code).
| <Link href={row.openHref} className="min-w-0"> | ||
| <Button | ||
| size="sm" | ||
| variant="hero" | ||
| className="auth-hero-cta h-auto w-full min-w-0 justify-center gap-1 whitespace-normal px-2 py-2 text-xs leading-tight sm:text-sm" | ||
| disabled={!canOpen} | ||
| > | ||
| <Brain className="h-4 w-4 shrink-0" /> | ||
| Open Deck Tree | ||
| </Button> | ||
| </Link> |
There was a problem hiding this comment.
<Link> wraps a disabled <Button>, but disabling the button does not prevent navigation via the link (users can still click/tap and navigate to openHref). If the intent is to block opening empty decks, conditionally render the <Link> only when canOpen is true (or apply aria-disabled + pointer-events: none on the link when disabled).
| <Link href={row.openHref} className="min-w-0"> | |
| <Button | |
| size="sm" | |
| variant="hero" | |
| className="auth-hero-cta h-auto w-full min-w-0 justify-center gap-1 whitespace-normal px-2 py-2 text-xs leading-tight sm:text-sm" | |
| disabled={!canOpen} | |
| > | |
| <Brain className="h-4 w-4 shrink-0" /> | |
| Open Deck Tree | |
| </Button> | |
| </Link> | |
| {canOpen ? ( | |
| <Link href={row.openHref} className="min-w-0"> | |
| <Button | |
| size="sm" | |
| variant="hero" | |
| className="auth-hero-cta h-auto w-full min-w-0 justify-center gap-1 whitespace-normal px-2 py-2 text-xs leading-tight sm:text-sm" | |
| > | |
| <Brain className="h-4 w-4 shrink-0" /> | |
| Open Deck Tree | |
| </Button> | |
| </Link> | |
| ) : ( | |
| <Button | |
| size="sm" | |
| variant="hero" | |
| className="auth-hero-cta h-auto w-full min-w-0 justify-center gap-1 whitespace-normal px-2 py-2 text-xs leading-tight sm:text-sm" | |
| disabled | |
| > | |
| <Brain className="h-4 w-4 shrink-0" /> | |
| Open Deck Tree | |
| </Button> | |
| )} |
| const startedRows = await prisma.courseProgress.findMany({ | ||
| where: { userId }, | ||
| where: { userId, archivedAt: null }, | ||
| select: { courseId: true }, | ||
| }) |
There was a problem hiding this comment.
This query filters on the new archivedAt column, but the file explicitly tries to remain usable “until migrations are applied”. If the DB hasn’t been migrated yet, referencing archivedAt on course_progress will throw and break the dashboard. To preserve the intended backward-compat behavior, wrap this in a try/catch with a fallback query (e.g., omit the archivedAt filter) or gate the filter behind a feature check.
| startTransition(async () => { | ||
| await postJson('/api/profile/archive', { | ||
| type: 'course', | ||
| courseSlug, | ||
| archiveLinkedDeck, | ||
| }) | ||
| router.refresh() | ||
| }) |
There was a problem hiding this comment.
postJson() throws on non-2xx responses, but these startTransition(async () => ...) handlers don’t catch errors. A 401/403 (not logged in) or 5xx will surface as an unhandled promise rejection and the UI won’t give any feedback. Wrap the transition body in try/catch and show a user-facing error (consistent with patterns like components/student/task-card.tsx).
| const courseIdFromTask = (task: any): string | null => { | ||
| const lesson = task?.lesson | ||
| if (!lesson || typeof lesson !== 'object') return null | ||
| const course = (lesson as { course?: unknown }).course | ||
| if (!course) return null | ||
| if (typeof course === 'string' || typeof course === 'number') return String(course) | ||
| if (typeof course === 'object' && 'id' in course) return String((course as { id: string | number }).id) | ||
| return null | ||
| } |
There was a problem hiding this comment.
courseIdFromTask() is now duplicated here and in app/api/practice/session/route.ts. To avoid the two implementations drifting (especially around the Payload task/lesson/course shapes), consider extracting this helper into a shared utility (e.g. lib/payload-task-helpers.ts) and importing it from both routes.
| const courseIdFromTask = (task: any): string | null => { | ||
| const lesson = task?.lesson | ||
| if (!lesson || typeof lesson !== 'object') return null | ||
| const course = (lesson as { course?: unknown }).course | ||
| if (!course) return null | ||
| if (typeof course === 'string' || typeof course === 'number') return String(course) | ||
| if (typeof course === 'object' && 'id' in course) return String((course as { id: string | number }).id) | ||
| return null | ||
| } |
There was a problem hiding this comment.
courseIdFromTask() is duplicated with the same helper in app/api/recommend/tasks/route.ts. Consider extracting it into a shared util to keep task→course parsing logic consistent across recommendation/practice endpoints.
…rses and decks when user resumes learning, enhancing user experience
This pull request introduces a comprehensive "archive/unarchive" feature for courses and flashcard decks in the learning platform. It adds backend API endpoints to archive and unarchive items, updates the UI to allow users to manage their archived content, and ensures that archived courses and decks are excluded from recommendations and practice sessions. Additionally, it improves the robustness of Docker builds and refines queries to handle archived content correctly.
Archive/Unarchive Functionality
POST /api/profile/archiveandPOST /api/profile/unarchiveto allow users to archive or unarchive courses and flashcard decks, with validation and upsert logic for related data. [1] [2]ArchiveCourseButton,ArchiveDeckButton,UnarchiveCourseButton,UnarchiveDeckButton) and integrated them into course, deck, and profile pages so users can archive/unarchive directly from the interface. (LearningPlatform/app/(student)/(shell)/courses/[slug]/page.tsxR16, LearningPlatform/app/(student)/(shell)/courses/[slug]/page.tsxR481-R484, [1] [2] [3]Excluding Archived Content from User Experience
Build Process Improvements
npm cistep more robust by adding retry logic and cache verification, reducing build failures due to transient network issues.These changes collectively provide users with fine-grained control over their learning content, improve the reliability of the build process, and ensure a cleaner, more relevant user experience by respecting archived states throughout the application.