Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
de519bd
Catalog to Planner Drag and Drop
liug88 Mar 31, 2026
e9ab14b
Revised Drag and Drop Logic.
liug88 Mar 31, 2026
5c9bed1
Fixing Copilot Requests
liug88 Mar 31, 2026
e3653ea
Prettier changes
liug88 Mar 31, 2026
25aa33a
Merge branch 'main-preview' into catalog-to-planner-dnd
liug88 Mar 31, 2026
7b2698d
Fixing Syntax Error and Prettier change
liug88 Mar 31, 2026
5ca443f
feat(catalog): unique draggable IDs and drag handle indicator
liug88 Apr 3, 2026
f75d7c1
fix(dnd): move catalog drop logic to handleDragOver and fix missing c…
liug88 Apr 3, 2026
2b55ffb
fix(catalog): disable drag on mobile and prevent list scroll during drag
liug88 Apr 3, 2026
12bdaee
fix(dnd): use per-drag UUID for catalog course to allow re-dragging
liug88 Apr 7, 2026
fe9bec0
fix(lint): fix import order in CatalogCourse and remove stale dep in dnd
liug88 Apr 7, 2026
f7bdaa4
fix(catalog): hide drag indicator on mobile, lock inner list scroll d…
liug88 Apr 10, 2026
91586b7
fix(dnd): fix overlay delay, semester shadow, and catalog placeholder…
liug88 Apr 10, 2026
61e085a
merge: merge main-preview into catalog-to-planner-dnd
Copilot Apr 14, 2026
1965e4d
fix(dnd): fix localStorage reset, add catalog-to-toolbox drag support
Copilot Apr 14, 2026
aa5f213
fix(dnd): replace em dash with double hyphen in comment
Copilot Apr 14, 2026
0809a04
Changing Opacity
liug88 Apr 17, 2026
f47f0db
refactor(dnd): unify catalog-drag placeholder with toolbox ghost cont…
liug88 Apr 17, 2026
8bee519
Deleting Unnecessary Context and refactoring DND
liug88 Apr 24, 2026
a677ba4
Merge remote-tracking branch 'origin/main-preview' into catalog-to-pl…
liug88 Apr 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 1 addition & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 55 additions & 5 deletions src/components/course/Course.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from "react";
import { useState, type HTMLAttributes } from "react";

import { useDragOperation } from "@dnd-kit/react";
import { useSortable } from "@dnd-kit/react/sortable";
import * as ContextMenu from "@radix-ui/react-context-menu";
import * as Popover from "@radix-ui/react-popover";
Expand Down Expand Up @@ -34,6 +35,15 @@ export default function Course({
}: CourseProps) {
const dndType = `${variant}-course`;

// The in-flight node for a catalog drag is rendered by us (not dnd-kit's
// sortable ghost), so read the active drag operation and flag ourselves as
// the placeholder when our id matches the placeholderId stashed on the
// source in handleDragStart.
const operation = useDragOperation();
const isCatalogPlaceholder =
operation?.source?.data?.type === "catalog-course" &&
operation.source.data.placeholderId === id;

const { handleRef, ref, isDragging } = useSortable({
id,
group,
Expand All @@ -49,6 +59,7 @@ export default function Course({
<ToolboxCourseView
innerRef={ref}
isDragging={isDragging}
isCatalogPlaceholder={isCatalogPlaceholder}
course={course}
/>
);
Expand All @@ -59,6 +70,7 @@ export default function Course({
innerRef={ref}
handleRef={handleRef}
isDragging={isDragging}
isCatalogPlaceholder={isCatalogPlaceholder}
course={course}
semesterId={semesterId}
/>
Expand All @@ -69,14 +81,21 @@ type ViewProps = {
innerRef?: (element: HTMLElement | null) => void;
handleRef?: (element: HTMLElement | null) => void;
isDragging: boolean;
isCatalogPlaceholder?: boolean;
course: UserCourse;
semesterId?: string | null;
};

function ToolboxCourseView({ innerRef, isDragging, course }: ViewProps) {
function ToolboxCourseView({
innerRef,
isDragging,
isCatalogPlaceholder = false,
course,
}: ViewProps) {
return (
<div
ref={innerRef}
{...getCatalogPlaceholderAttrs(isCatalogPlaceholder)}
data-shadow={isDragging || undefined}
className="relative bg-carpipink text-nowrap rounded-md w-fit px-3 py-1 hover:cursor-grab active:cursor-grabbing select-none"
>
Expand All @@ -86,13 +105,39 @@ function ToolboxCourseView({ innerRef, isDragging, course }: ViewProps) {
);
}

// Catalog drags mount a real <Course /> as the in-flight placeholder. dnd-kit
// doesn't know about that element, so we manually apply the same contract it
// uses for its own sortable placeholders (`data-dnd-placeholder="clone"` +
// `inert` + `aria-hidden` + `tabIndex=-1`). That lets the existing CSS rule
// (`[data-dnd-placeholder="clone"] { opacity: 0.5 !important }`) and `inert`
// handle the ghost appearance and interaction-suppression uniformly -- the
// same way the toolbox-originated placeholder works out of the box.
// `inert` typing wasn't added until React 19, hence the cast.
function getCatalogPlaceholderAttrs(
isCatalogPlaceholder: boolean,
): HTMLAttributes<HTMLDivElement> {
return (
isCatalogPlaceholder
? {
"data-dnd-placeholder": "clone",
inert: "",
"aria-hidden": true,
tabIndex: -1,
}
: {}
) as HTMLAttributes<HTMLDivElement>;
}

function PlannerCourseView({
innerRef,
handleRef,
isDragging,
isCatalogPlaceholder = false,
course,
semesterId,
}: ViewProps) {
const placeholderAttrs = getCatalogPlaceholderAttrs(isCatalogPlaceholder);
const isGhost = isDragging || isCatalogPlaceholder;
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const menuOptions = usePlannerCourse({
course,
Expand All @@ -118,6 +163,7 @@ function PlannerCourseView({
<ContextMenu.Trigger disabled={!isTouch} asChild>
<div
ref={innerRef}
{...placeholderAttrs}
onContextMenu={(e) => {
if (
typeof window !== "undefined" &&
Expand All @@ -130,11 +176,11 @@ function PlannerCourseView({
className={cn(
"relative flex justify-between bg-darkblue rounded-2xl text-carpipink gap-4 px-2 py-3",
"hover:shadow-lg",
isDragging ? "cursor-grabbing" : "cursor-grab",
isGhost ? "cursor-grabbing" : "cursor-grab",
)}
>
<div className="flex gap-2 items-center">
<button ref={handleRef}>
<button ref={handleRef} type="button" aria-label="Drag to reorder">
<MdDragIndicator size={22} />
</button>
<CourseLabel course={course.data} showCredits />
Expand All @@ -161,7 +207,11 @@ function PlannerCourseView({
onOpenChange={setPopoverOpen}
>
<Popover.Trigger asChild>
<button className="outline-none">
<button
type="button"
className="outline-none"
aria-label="Course options"
>
<MdOutlineMoreHoriz className="cursor-pointer text-2xl" />
</button>
</Popover.Trigger>
Expand Down
53 changes: 41 additions & 12 deletions src/features/catalog/components/CatalogCourse.tsx
Comment thread
liug88 marked this conversation as resolved.
Comment thread
liug88 marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar to the lack of a planner course placeholder, I also just noticed that there is a lack of a toolbox course placeholder when hovering a course that came from the catalog over the toolbox.

Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { useState } from "react";
import { useState, useRef } from "react";

import { useDraggable } from "@dnd-kit/react";
import { motion } from "framer-motion";
import { IoAdd } from "react-icons/io5";
import { MdDragIndicator } from "react-icons/md";
import { v4 as uuidv4 } from "uuid";

import CourseBadge from "@/components/course/CourseBadge";
import CourseLabel from "@/components/course/CourseLabel";
import Tag from "@/components/Tag";
import { useCourseWorkspace } from "@/core/workspace/useCourseWorkspace";
import useIsDesktop from "@/lib/hooks/useIsDesktop";
import { useCourseFilters } from "@/lib/stores/useFilterStore";
import { APICourse } from "@/lib/types";

Expand All @@ -18,6 +22,16 @@ const Course: React.FC<CourseProps> = ({ course }) => {
const { addCourseToToolbox, getCourseCount } = useCourseWorkspace();
const { attrFilters, semFilters } = useCourseFilters(course);

const isDesktop = useIsDesktop();
const idRef = useRef(uuidv4());

const { ref, isDragging } = useDraggable({
Comment thread
liug88 marked this conversation as resolved.
id: idRef.current,
type: "catalog-course",
data: { type: "catalog-course", course },
disabled: !isDesktop,
});

const [isOpen, setIsOpen] = useState(false);
const toggleOpen = (e: React.MouseEvent<HTMLDivElement>) => {
const target = e.target as HTMLElement;
Expand All @@ -26,7 +40,8 @@ const Course: React.FC<CourseProps> = ({ course }) => {

return (
<div
className="hover:cursor-pointer hover:bg-darkblue/10 border border-black rounded-xl w-full p-4 relative"
ref={ref}
className={`relative bg-carpipink hover:cursor-pointer hover:bg-darkblue/10 border-1 border-black rounded-xl w-full p-4 ${isDragging ? "opacity-50" : ""}`}
onClick={toggleOpen}
>
Comment thread
liug88 marked this conversation as resolved.
<CourseBadge
Expand All @@ -35,18 +50,30 @@ const Course: React.FC<CourseProps> = ({ course }) => {
/>

<div className={`flex items-center justify-between`}>
<div>
<CourseLabel course={course} showCredits />
<div className={`flex flex-wrap mt-1 gap-1`}>
{attrFilters.map((attr, index) => {
return <Tag key={index} filter={attr} />;
})}
{semFilters?.map((semester, index) => {
return <Tag key={index} filter={semester} />;
})}
<div className="flex items-start gap-2">
{isDesktop && (
<MdDragIndicator
size={20}
className="shrink-0 mt-0.5 text-darkblue/40"
/>
)}
<div>
<CourseLabel course={course} showCredits />
<div className={`flex flex-wrap mt-1 gap-1`}>
{attrFilters.map((attr, index) => {
return <Tag key={index} filter={attr} />;
})}
{semFilters?.map((semester, index) => {
return <Tag key={index} filter={semester} />;
})}
</div>
</div>
</div>
<div id="add-button" className={``}>
<div
id="add-button"
className={``}
onPointerDown={(e) => e.stopPropagation()}
>
<AddButton
addCourse={(e) => {
e.stopPropagation();
Expand Down Expand Up @@ -81,6 +108,8 @@ interface AddButtonProps {
const AddButton: React.FC<AddButtonProps> = ({ addCourse }) => {
return (
<button
type="button"
aria-label="Add course to toolbox"
className={`hover:cursor-pointer hover:bg-darkblue hover:text-carpipink border-2 border-darkblue rounded-full p-1 text-4xl`}
onClick={addCourse}
>
Expand Down
11 changes: 9 additions & 2 deletions src/features/catalog/components/CatalogResults.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useDragOperation } from "@dnd-kit/react";

import Course from "@/features/catalog/components/CatalogCourse";
import { useCatalog } from "@/features/catalog/search/context/useCatalog";
import DepartmentFilters from "@/features/catalog/search/filters/DepartmentFilters";
Expand All @@ -11,10 +13,15 @@ export default function CatalogResults() {
selectedFilters,
} = useCatalog();

const operation = useDragOperation();
const isDraggingCatalog = operation?.source?.data?.type === "catalog-course";

return (
<div className="overflow-y-auto">
<div className={isDraggingCatalog ? "overflow-hidden" : "overflow-y-auto"}>
Comment thread
mirmirmirr marked this conversation as resolved.
{searchResults.length > 0 ? (
<div className="h-full overflow-y-auto flex flex-wrap justify-center gap-4 pr-3 pt-3">
<div
className={`h-full ${isDraggingCatalog ? "overflow-hidden" : "overflow-y-auto"} flex flex-wrap justify-center gap-4 pr-3 pt-3`}
>
{searchResults.map((course, index) => (
<Course key={index} course={course} />
))}
Expand Down
Loading
Loading