From 5b163a85cf466f42007810ef694812edc406929b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emre=20S=C3=B6zbilir?= Date: Tue, 7 Jul 2026 21:36:34 +0200 Subject: [PATCH 1/3] Improve course save and planner detail UX --- .../courses/components/CourseDetailDrawer.tsx | 9 +- .../features/courses/components/Overview.tsx | 8 +- .../features/favorites/FavoritesContext.ts | 1 + .../components/FavoritesProvider.tsx | 17 ++- .../features/favorites/utils/favoriteIds.ts | 18 +++ .../components/PlannerFavoritesPanel.tsx | 24 +++- .../planner/components/SemesterPlanner.tsx | 11 +- frontend/src/shared/components/CourseCard.tsx | 9 +- frontend/src/shared/components/FavStar.tsx | 14 ++- frontend/src/shared/components/catClasses.ts | 104 +++++++++--------- frontend/tests/favorites/favoriteIds.test.ts | 23 ++++ 11 files changed, 164 insertions(+), 74 deletions(-) create mode 100644 frontend/src/features/favorites/utils/favoriteIds.ts create mode 100644 frontend/tests/favorites/favoriteIds.test.ts diff --git a/frontend/src/features/courses/components/CourseDetailDrawer.tsx b/frontend/src/features/courses/components/CourseDetailDrawer.tsx index edb6aac..0c210bb 100644 --- a/frontend/src/features/courses/components/CourseDetailDrawer.tsx +++ b/frontend/src/features/courses/components/CourseDetailDrawer.tsx @@ -17,6 +17,7 @@ interface CourseDetailDrawerProps { listCourse?: Course | null isFavorite: boolean favoriteDisabled?: boolean + favoriteLoading?: boolean showFavorite?: boolean onToggleFavorite: () => void onClose: () => void @@ -27,6 +28,7 @@ export function CourseDetailDrawer({ listCourse = null, isFavorite, favoriteDisabled = false, + favoriteLoading = false, showFavorite = true, onToggleFavorite, onClose, @@ -78,7 +80,12 @@ export function CourseDetailDrawer({
{showFavorite ? ( - + ) : null}
event.stopPropagation()}> - onOpenCourse(course.id)} aria-label={t('planner.favorites.openCourseDetail')} className="flex items-center justify-center rounded-md p-1 text-fg-muted transition-colors hover:bg-surface-hover hover:text-primary" > - - onToggleFavorite(course.id)} /> + + onToggleFavorite(course.id)} + />
@@ -178,7 +186,9 @@ interface PlannerFavoritesPanelProps { catalogTo?: string onSetAssignment: (courseId: string, areaCode: string | null) => void onAddCourse: (courseId: string, areaCode: string | null) => void + onOpenCourse: (courseId: string) => void onToggleFavorite: (courseId: string) => void + isFavoriteSaving: (courseId: string) => boolean hiddenSlotIds: string[] onSelectTutorialSlot: (courseId: string, selectedSlotId: string) => void } @@ -200,7 +210,9 @@ export function PlannerFavoritesPanel({ catalogTo = ROUTES.catalog, onSetAssignment, onAddCourse, + onOpenCourse, onToggleFavorite, + isFavoriteSaving, hiddenSlotIds, onSelectTutorialSlot, }: PlannerFavoritesPanelProps) { @@ -259,7 +271,9 @@ export function PlannerFavoritesPanel({ activeSemesterLabel={activeSemesterLabel} hiddenSlotIds={hiddenSlotIds} onAddCourse={onAddCourse} + onOpenCourse={onOpenCourse} onToggleFavorite={onToggleFavorite} + isFavoriteSaving={isFavoriteSaving} onSelectTutorialSlot={onSelectTutorialSlot} /> diff --git a/frontend/src/features/planner/components/SemesterPlanner.tsx b/frontend/src/features/planner/components/SemesterPlanner.tsx index 0933714..bb4a434 100644 --- a/frontend/src/features/planner/components/SemesterPlanner.tsx +++ b/frontend/src/features/planner/components/SemesterPlanner.tsx @@ -68,7 +68,7 @@ export function SemesterPlanner({ const { isAuthenticated, token, user } = useAuth() const { t } = useTranslation() const { isOpen: isOnboardingOpen, activeStepId } = useOnboarding() - const { favoriteIds, toggleFavorite } = useFavorites() + const { favoriteIds, isFavoriteSaving, toggleFavorite } = useFavorites() const { completedCourses, isLoadingCompletedCourses, @@ -290,6 +290,13 @@ export function SemesterPlanner({ } } + function handleOpenFavoriteCourse(courseId: string): void { + setOpenCourseId(courseId) + if (isSmallViewport) { + setIsAddDrawerOpen(false) + } + } + function handleAddManualSlot(slot: ManualPlannerSlot): void { if (!plannedCourseIds.includes(slot.courseId)) { setPlannedCourseIds([...plannedCourseIds, slot.courseId]) @@ -452,7 +459,9 @@ export function SemesterPlanner({ maxVisibleCandidates={isPlannerMobileInterestedTour ? 2 : undefined} onSetAssignment={setAssignment} onAddCourse={handleInterestedCourseAdd} + onOpenCourse={handleOpenFavoriteCourse} onToggleFavorite={toggleFavorite} + isFavoriteSaving={isFavoriteSaving} hiddenSlotIds={displayHiddenSlotIds} onSelectTutorialSlot={handleTutorialSlotSelect} /> diff --git a/frontend/src/shared/components/CourseCard.tsx b/frontend/src/shared/components/CourseCard.tsx index 345d74e..993270c 100644 --- a/frontend/src/shared/components/CourseCard.tsx +++ b/frontend/src/shared/components/CourseCard.tsx @@ -24,6 +24,7 @@ interface CourseCardProps { isActive?: boolean isCompleted?: boolean favoriteDisabled?: boolean + favoriteLoading?: boolean showFavorite?: boolean offeringStatus?: OfferingStatus // Overrides the raw course.termType so callers can align season tags with @@ -57,6 +58,7 @@ export function CourseCard({ isActive = false, isCompleted = false, favoriteDisabled = false, + favoriteLoading = false, showFavorite = true, offeringStatus = 'confirmed', seasonTermType, @@ -126,7 +128,12 @@ export function CourseCard({ }} onKeyDown={(event) => event.stopPropagation()} > - + ) : undefined } diff --git a/frontend/src/shared/components/FavStar.tsx b/frontend/src/shared/components/FavStar.tsx index 9606fe6..8aefdc6 100644 --- a/frontend/src/shared/components/FavStar.tsx +++ b/frontend/src/shared/components/FavStar.tsx @@ -4,12 +4,13 @@ import { BookmarkIcon } from './icons' interface FavStarProps { active: boolean disabled?: boolean + isLoading?: boolean onToggle: () => void } -export function FavStar({ active, disabled = false, onToggle }: FavStarProps) { +export function FavStar({ active, disabled = false, isLoading = false, onToggle }: FavStarProps) { const [isHovered, setIsHovered] = useState(false) - const showFilled = isHovered ? !active : active + const showFilled = isLoading ? active : isHovered ? !active : active return ( diff --git a/frontend/src/shared/components/catClasses.ts b/frontend/src/shared/components/catClasses.ts index 8d74206..b28dc59 100644 --- a/frontend/src/shared/components/catClasses.ts +++ b/frontend/src/shared/components/catClasses.ts @@ -1,52 +1,52 @@ -import type { MasterCat } from '../../features/courses' - -/** Muted area tints — readable category hint without loud saturation. */ -export const CAT_BADGE_CLASSES: Record = { - TECH: 'text-cat-tech/45 border-cat-tech/18 bg-cat-tech/5', - THEO: 'text-cat-theo/42 border-cat-theo/16 bg-cat-theo/4', - PRAK: 'text-cat-prak/45 border-cat-prak/18 bg-cat-prak/5', - INFO: 'text-cat-info/55 border-cat-info/25 bg-cat-info/10 dark:text-[#c4a8e8] dark:border-[#9b7cc4]/45 dark:bg-cat-info/20', - BASIS: 'text-cat-basis/42 border-cat-basis/16 bg-cat-basis/4', -} - -/** Matches tag intensity — soft fills for credited ECTS in the auto-assign panel. */ -export const CAT_PROGRESS_CREDITED_CLASSES: Record = { - TECH: 'bg-cat-tech/18', - THEO: 'bg-cat-theo/15', - PRAK: 'bg-cat-prak/18', - INFO: 'bg-cat-info/15', - BASIS: 'bg-cat-basis/15', -} - -export const CAT_PROGRESS_DOT_CLASSES: Record = { - TECH: 'bg-cat-tech/40', - THEO: 'bg-cat-theo/38', - PRAK: 'bg-cat-prak/40', - INFO: 'bg-cat-info/38', - BASIS: 'bg-cat-basis/38', -} - -export function catProgressPlannedStyle(masterCat: MasterCat | null): { - backgroundColor: string - backgroundImage: string - opacity: number -} { - const baseColor = masterCat === 'TECH' - ? 'var(--color-cat-tech)' - : masterCat === 'THEO' - ? 'var(--color-cat-theo)' - : masterCat === 'PRAK' - ? 'var(--color-cat-prak)' - : masterCat === 'INFO' - ? 'var(--color-cat-info)' - : masterCat === 'BASIS' - ? 'var(--color-cat-basis)' - : 'var(--color-border)' - return { - backgroundColor: baseColor, - backgroundImage: - 'repeating-linear-gradient(135deg, transparent, transparent 3px, rgba(255,255,255,0.5) 3px, rgba(255,255,255,0.5) 6px)', - opacity: 0.22, - } -} - \ No newline at end of file +import type { MasterCat } from '../../features/courses' + +/** Muted area tints — readable category hint without loud saturation. */ +export const CAT_BADGE_CLASSES: Record = { + TECH: 'border-[#B8D9E6] bg-[#EEF8FB] text-[#315F73] dark:border-[#4A6470] dark:bg-[#253239] dark:text-[#A8C7D3]', + THEO: 'border-[#EBC3D6] bg-[#FCF0F5] text-[#8A4968] dark:border-[#714B5E] dark:bg-[#382932] dark:text-[#D8A9C0]', + PRAK: 'border-[#B7E1DA] bg-[#EFF9F6] text-[#3D6E66] dark:border-[#486B65] dark:bg-[#263633] dark:text-[#A8D2CA]', + INFO: 'border-[#D3C4E6] bg-[#F6F1FB] text-[#654F83] dark:border-[#5E526F] dark:bg-[#302A39] dark:text-[#C4B4D9]', + BASIS: 'border-[#E8C7C4] bg-[#FCF1EF] text-[#8A504B] dark:border-[#704F4B] dark:bg-[#392B29] dark:text-[#D9ABA6]', +} + +/** Matches tag intensity — soft fills for credited ECTS in the auto-assign panel. */ +export const CAT_PROGRESS_CREDITED_CLASSES: Record = { + TECH: 'bg-cat-tech/18', + THEO: 'bg-cat-theo/15', + PRAK: 'bg-cat-prak/18', + INFO: 'bg-cat-info/15', + BASIS: 'bg-cat-basis/15', +} + +export const CAT_PROGRESS_DOT_CLASSES: Record = { + TECH: 'bg-cat-tech/40', + THEO: 'bg-cat-theo/38', + PRAK: 'bg-cat-prak/40', + INFO: 'bg-cat-info/38', + BASIS: 'bg-cat-basis/38', +} + +export function catProgressPlannedStyle(masterCat: MasterCat | null): { + backgroundColor: string + backgroundImage: string + opacity: number +} { + const baseColor = masterCat === 'TECH' + ? 'var(--color-cat-tech)' + : masterCat === 'THEO' + ? 'var(--color-cat-theo)' + : masterCat === 'PRAK' + ? 'var(--color-cat-prak)' + : masterCat === 'INFO' + ? 'var(--color-cat-info)' + : masterCat === 'BASIS' + ? 'var(--color-cat-basis)' + : 'var(--color-border)' + return { + backgroundColor: baseColor, + backgroundImage: + 'repeating-linear-gradient(135deg, transparent, transparent 3px, rgba(255,255,255,0.5) 3px, rgba(255,255,255,0.5) 6px)', + opacity: 0.22, + } +} + diff --git a/frontend/tests/favorites/favoriteIds.test.ts b/frontend/tests/favorites/favoriteIds.test.ts new file mode 100644 index 0000000..9a664da --- /dev/null +++ b/frontend/tests/favorites/favoriteIds.test.ts @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { toggleFavoriteId, updateSavingFavoriteIds } from '../../src/features/favorites/utils/favoriteIds.ts' + +test('toggleFavoriteId adds and removes one course without mutating the input', () => { + const favoriteIds = ['course-a'] + + const withAddedCourse = toggleFavoriteId(favoriteIds, 'course-b') + const withRemovedCourse = toggleFavoriteId(favoriteIds, 'course-a') + + assert.deepEqual(withAddedCourse, ['course-a', 'course-b']) + assert.deepEqual(withRemovedCourse, []) + assert.deepEqual(favoriteIds, ['course-a']) +}) + +test('updateSavingFavoriteIds tracks one saving entry per course', () => { + const savingFavoriteIds = updateSavingFavoriteIds(['course-a'], 'course-a', true) + + assert.deepEqual(savingFavoriteIds, ['course-a']) + assert.deepEqual(updateSavingFavoriteIds(savingFavoriteIds, 'course-b', true), ['course-a', 'course-b']) + assert.deepEqual(updateSavingFavoriteIds(savingFavoriteIds, 'course-a', false), []) +}) From f978989fb3e22f61ae04145d79979b6fbc2f7125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emre=20S=C3=B6zbilir?= Date: Tue, 7 Jul 2026 21:44:36 +0200 Subject: [PATCH 2/3] Polish course save feedback and manual slot dialog --- .../planner/components/ManualSlotDialog.tsx | 18 ++--- frontend/src/index.css | 80 +++++++++++++++++++ frontend/src/shared/components/FavStar.tsx | 17 +++- 3 files changed, 104 insertions(+), 11 deletions(-) diff --git a/frontend/src/features/planner/components/ManualSlotDialog.tsx b/frontend/src/features/planner/components/ManualSlotDialog.tsx index 555100e..1b7d631 100644 --- a/frontend/src/features/planner/components/ManualSlotDialog.tsx +++ b/frontend/src/features/planner/components/ManualSlotDialog.tsx @@ -49,7 +49,7 @@ export function ManualSlotDialog({ courses, onClose, onAdd }: ManualSlotDialogPr role="dialog" aria-modal="true" aria-labelledby="manual-slot-title" - className="w-full max-w-md rounded-[12px] border border-border bg-surface p-5 shadow-xl" + className="w-full max-w-md min-w-0 overflow-hidden rounded-[12px] border border-border bg-surface p-5 shadow-xl" >

Add manual time slot @@ -58,13 +58,13 @@ export function ManualSlotDialog({ courses, onClose, onAdd }: ManualSlotDialogPr Place a course without catalog times — or a custom appointment — on the weekly grid.

-
-