Skip to content

feat(courses): add unarchive functionality and toast notifications#25

Merged
Z4phxr merged 2 commits intomainfrom
feat/archive
Apr 25, 2026
Merged

feat(courses): add unarchive functionality and toast notifications#25
Z4phxr merged 2 commits intomainfrom
feat/archive

Conversation

@Z4phxr
Copy link
Copy Markdown
Owner

@Z4phxr Z4phxr commented Apr 25, 2026

This pull request enhances the course and deck archiving experience for students by introducing user feedback to archiving actions and enabling unarchiving of courses directly from the course page. The changes focus on improving user interaction and providing immediate confirmation of actions.

User feedback improvements:

  • Introduced an ActionToast component and a useActionToast hook to display temporary toast messages after archiving or unarchiving actions, providing clear feedback to users. Toasts are now shown after archiving/unarchiving courses and decks in all related action buttons (ArchiveCourseButton, ArchiveDeckButton, UnarchiveCourseButton, UnarchiveDeckButton). [1] [2] [3] [4] [5]

Course page enhancements:

Component and import updates:

Copilot AI review requested due to automatic review settings April 25, 2026 19:11
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds student-facing unarchive actions and introduces lightweight toast notifications to confirm archive/unarchive operations across course/deck UI, including conditional archive vs unarchive controls on the course page.

Changes:

  • Added an inline ActionToast component + useActionToast hook and wired to archive/unarchive buttons.
  • Added archived-state lookup on the course page to render UnarchiveCourseButton when applicable.
  • Updated course page imports to include UnarchiveCourseButton.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
LearningPlatform/components/profile/archive-actions.tsx Adds toast UI + hook and shows toasts after archive/unarchive actions.
LearningPlatform/app/(student)/(shell)/courses/[slug]/page.tsx Checks whether the current user has archived the course and conditionally renders archive/unarchive action.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 73 to 81
startTransition(async () => {
try {
await postJson('/api/profile/archive', {
type: 'course',
courseSlug,
archiveLinkedDeck,
})
router.refresh()
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to archive course')
}
await postJson('/api/profile/archive', {
type: 'course',
courseSlug,
archiveLinkedDeck,
})
showToast('Course archived')
router.refresh()
})
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the toast state is local to each button component, calling router.refresh() right after showToast() can cause the component to unmount (e.g. when the parent server component re-renders and the archived item disappears / the button switches from Archive→Unarchive). In those cases the toast will be cleared immediately and users may not see any feedback. Consider moving the toast state/host to a stable parent (context/provider/portal) that survives refreshes, or delaying the refresh until after the toast duration.

Copilot uses AI. Check for mistakes.
Comment on lines +494 to +498
{courseArchived ? (
<UnarchiveCourseButton courseId={courseId} />
) : (
<ArchiveCourseButton courseSlug={slug} />
)}
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The course page swaps ↔ based on courseArchived, and the button components own the toast state. After an archive/unarchive click, router.refresh() will re-render the page and replace the button component, unmounting it and clearing the toast state—so the toast may flash briefly or not be visible at all. Consider hoisting the toast host/state to a parent that won’t unmount on refresh (e.g. a layout-level toast provider), or keep a stable client wrapper around this section and render the toast there instead of inside the button components.

Suggested change
{courseArchived ? (
<UnarchiveCourseButton courseId={courseId} />
) : (
<ArchiveCourseButton courseSlug={slug} />
)}
<div
className={courseArchived ? 'hidden' : undefined}
aria-hidden={courseArchived}
tabIndex={courseArchived ? -1 : undefined}
>
<ArchiveCourseButton courseSlug={slug} />
</div>
<div
className={courseArchived ? undefined : 'hidden'}
aria-hidden={!courseArchived}
tabIndex={!courseArchived ? -1 : undefined}
>
<UnarchiveCourseButton courseId={courseId} />
</div>

Copilot uses AI. Check for mistakes.
Comment on lines +152 to +155
const archivedCourse = await prisma.courseProgress.findFirst({
where: {
userId: session.user.id,
courseId,
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CourseProgress has a @@unique([userId, courseId]), so this lookup should use findUnique({ where: { userId_courseId: ... } }) instead of findFirst. Using findUnique makes the intent clearer and lets Prisma take advantage of the unique constraint/index.

Suggested change
const archivedCourse = await prisma.courseProgress.findFirst({
where: {
userId: session.user.id,
courseId,
const archivedCourse = await prisma.courseProgress.findUnique({
where: {
userId_courseId: {
userId: session.user.id,
courseId,
},

Copilot uses AI. Check for mistakes.
<div
role="status"
aria-live="polite"
className="fixed bottom-4 left-1/2 z-50 -translate-x-1/2 rounded-md border border-black/10 bg-black/85 px-3 py-2 text-xs text-white shadow-lg"
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This toast is position: fixed and can intercept pointer events while visible. Adding pointer-events-none (or otherwise ensuring it doesn’t capture clicks) prevents it from blocking underlying UI controls for ~2.2s, which is especially noticeable on small screens.

Suggested change
className="fixed bottom-4 left-1/2 z-50 -translate-x-1/2 rounded-md border border-black/10 bg-black/85 px-3 py-2 text-xs text-white shadow-lg"
className="pointer-events-none fixed bottom-4 left-1/2 z-50 -translate-x-1/2 rounded-md border border-black/10 bg-black/85 px-3 py-2 text-xs text-white shadow-lg"

Copilot uses AI. Check for mistakes.
@Z4phxr Z4phxr merged commit 59db0ae into main Apr 25, 2026
4 checks passed
@Z4phxr Z4phxr deleted the feat/archive branch April 25, 2026 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants