-
Notifications
You must be signed in to change notification settings - Fork 0
feat(courses): add unarchive functionality and toast notifications #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,7 +13,7 @@ import { prisma } from '@/lib/prisma' | |||||||||||||||||||||||||||||||||||||||
| import { CourseHeroTitle } from '@/components/courses/course-hero-title' | ||||||||||||||||||||||||||||||||||||||||
| import { studentGlassCard, studentGlassPill } from '@/lib/student-glass-styles' | ||||||||||||||||||||||||||||||||||||||||
| import { cn } from '@/lib/utils' | ||||||||||||||||||||||||||||||||||||||||
| import { ArchiveCourseButton } from '@/components/profile/archive-actions' | ||||||||||||||||||||||||||||||||||||||||
| import { ArchiveCourseButton, UnarchiveCourseButton } from '@/components/profile/archive-actions' | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** Same visual as dashboard `StatPill` in `flashcard-section.tsx` (server-safe duplicate). */ | ||||||||||||||||||||||||||||||||||||||||
| function CourseFlashcardStatPill({ label, count }: { label: string; count: number }) { | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -147,6 +147,17 @@ export default async function CoursePage({ params }: { params: Promise<{ slug: s | |||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const courseId = String(course.id) | ||||||||||||||||||||||||||||||||||||||||
| let courseArchived = false | ||||||||||||||||||||||||||||||||||||||||
| if (session?.user?.id) { | ||||||||||||||||||||||||||||||||||||||||
| const archivedCourse = await prisma.courseProgress.findFirst({ | ||||||||||||||||||||||||||||||||||||||||
| where: { | ||||||||||||||||||||||||||||||||||||||||
| userId: session.user.id, | ||||||||||||||||||||||||||||||||||||||||
| courseId, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| select: { archivedAt: true }, | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
| courseArchived = Boolean(archivedCourse?.archivedAt) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| const moduleIds = modulesWithLessons.map((m) => String(m.id)) | ||||||||||||||||||||||||||||||||||||||||
| let mainDeck = await prisma.flashcardDeck.findFirst({ | ||||||||||||||||||||||||||||||||||||||||
| where: { courseId, parentDeckId: null }, | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -480,7 +491,11 @@ export default async function CoursePage({ params }: { params: Promise<{ slug: s | |||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| <section className="flex justify-center pt-2"> | ||||||||||||||||||||||||||||||||||||||||
| <ArchiveCourseButton courseSlug={slug} /> | ||||||||||||||||||||||||||||||||||||||||
| {courseArchived ? ( | ||||||||||||||||||||||||||||||||||||||||
| <UnarchiveCourseButton courseId={courseId} /> | ||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||
| <ArchiveCourseButton courseSlug={slug} /> | ||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+494
to
+498
|
||||||||||||||||||||||||||||||||||||||||
| {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> |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||
| 'use client' | ||||||
|
|
||||||
| import { useState, useTransition } from 'react' | ||||||
| import { useEffect, useRef, useState, useTransition } from 'react' | ||||||
| import { useRouter } from 'next/navigation' | ||||||
| import { Button } from '@/components/ui/button' | ||||||
|
|
||||||
|
|
@@ -16,138 +16,160 @@ async function postJson(url: string, body: unknown) { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| function ActionToast({ message }: { message: string }) { | ||||||
| return ( | ||||||
| <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" | ||||||
|
||||||
| 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
AI
Apr 25, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CourseProgresshas a@@unique([userId, courseId]), so this lookup should usefindUnique({ where: { userId_courseId: ... } })instead offindFirst. UsingfindUniquemakes the intent clearer and lets Prisma take advantage of the unique constraint/index.