Skip to content

feat: section swap page#224

Open
jerryzhou196 wants to merge 46 commits into
mainfrom
feature/section-swap
Open

feat: section swap page#224
jerryzhou196 wants to merge 46 commits into
mainfrom
feature/section-swap

Conversation

@jerryzhou196

@jerryzhou196 jerryzhou196 commented May 21, 2026

Copy link
Copy Markdown
Member

Visit it on the /swap endpoint in the vercel preview

image

Relies on UWFlow/uwflow#184

Summary

Adds a /swap page where you can view your weekly schedule and swap individual course sections without juggling Quest tabs.

  • Weekly calendar of your enrolled schedule on the new /swap route
  • Section finder panel — click a class to see all of its sections with conflict detection, seat counts, and prof ratings
  • Course swap dropdown to pull in a different course and compare its sections, with a ghost preview on the calendar and a reset once you've swapped
  • "Section swap" entry added to the profile dropdown nav, plus an announcement banner linking to it

State handling depending on auth/schedule:

  • Logged out: a non-interactive sample schedule renders behind a login lock card ("Log in to continue")
  • Logged in without a saved schedule: the Quest upload modal sits over the calendar; the schedule persists after upload
  • Logged in with a saved schedule: loads straight into your enrolled sections

Built entirely in Tailwind (no styled-components) so it can serve as the starting point for the migration.

Test plan

  • Logged out: sample schedule shows behind the lock card; "Log in to continue" opens the auth modal
  • Logged in, no saved schedule: Quest upload modal appears; schedule persists after upload
  • Logged in with a schedule: calendar loads with your enrolled sections
  • Click a class to open the section finder; verify conflict badges, seat counts, and prof ratings
  • Use the swap dropdown to search another course, hover a section for the ghost preview, swap, then reset
  • Body scroll is locked while the overlay is up and restored after upload

Adds /swap route with a weekly calendar view of the user's schedule,
section finder panel, and a Quest import overlay for users without a
schedule. Includes ephemeral (unauthenticated) schedule support and
a "Section swap" link in the profile dropdown.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jerryzhou196 jerryzhou196 force-pushed the feature/section-swap branch from 2e1ce0e to 5e6f7a8 Compare May 21, 2026 12:13
jerryzhou196 and others added 4 commits May 21, 2026 14:53
- Show enrollment as filled/capacity (e.g. 53/90) instead of "X of Y open"
- Add Last Updated timestamp above section list
- Conditionally link prof name to their page only if they exist in DB
- Add updated_at field to swap course GQL query and SwapSection type
- Add fontSize prop to LastUpdatedSchedule for flexible sizing
- Add ProfNameText styled component for profs without a UWFlow profile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added align-items: center to the ContentWrapper styled component for improved layout consistency.
Stack steps vertically and shrink fixed-width upload/picture/video
boxes on screens <= 800px so the /welcome onboarding fits on mobile.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread src/components/upload/styles/DataUploadModals.tsx Outdated
Comment thread src/graphql/queries/course/SwapCourse.tsx Outdated
Comment thread src/pages/swapPage/styles/SectionFinderPanel.tsx Outdated
Comment thread src/pages/swapPage/CourseSearchDropdown.tsx Outdated
jerryzhou196 and others added 10 commits June 2, 2026 20:37
Replace the deprecated split Apollo 2.x packages (apollo-client,
apollo-link-*, apollo-cache-inmemory, react-apollo, @apollo/react-*)
with the unified @apollo/client v3.14.1, and bump graphql 14 -> 16.

- Hooks / ApolloProvider / ApolloQueryResult now import from
  @apollo/client; links from @apollo/client/link/{context,error}.
- Refactor the legacy <Query> render-prop in ProfileDropdown to useQuery
  (render-prop components are removed in Apollo v4).
- Extract the InMemoryCache dataIdFromObject into a unit-testable module
  and add a Jest suite covering every custom + default cache key.
- codegen.js gains reactApolloVersion: 3 so regenerated output uses
  `import * as Apollo from '@apollo/client'`.

Webpack 4 build fix: graphql 16 ships untranspiled class fields, so add
@babel/plugin-proposal-class-properties to the node_modules babel pass.

Jest-on-modern-Node fixes: pin graceful-fs to 4.2.11 (jest 24's copy
crashes on Node >=20) and treat node_modules .cjs/.mjs as modules (not
assets) so @apollo/client's ESM-flagged package loads under Jest.

Stay on @apollo/client v3 (not v4) until the Vite/Webpack 5 migration:
v4 is ESM-only with an exports map Webpack 4 cannot resolve.

Verified: tsc --noEmit, bun run lint-nofix, bun run test (8/8) and a
full Webpack 4 production build all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the profile-page week grid into a presentational, domain-free
<Calendar> at src/components/calendar, styled with Tailwind instead of
styled-components. ProfileCalendar now owns the Moment/schedule math and
maps each ScheduleInterval onto a CalendarEvent.

The component exposes per-event `variant` (section colour) and `state`
(default | selected | dimmed | preview), plus an `interactive` flag and a
`header` slot, so the upcoming section-swap page can reuse it directly:
preview a candidate section as a blurred dashed ghost, dim the events it
would replace, and ring the selected class -- without re-implementing the
grid.

Removes the now-dead src/pages/profilePage/Calendar.tsx and its styles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolved conflicts preferring main, except ProfileCalendar.tsx imports, which were kept from the branch so the merged calendar-refactor body still compiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…taIdFromObject

Address review comments on #246:

- Move the week-navigation header (date range, hours-this-week, and the
  prev / next / current-week controls) into the generic Calendar via a
  showHeader toggle + header*/onPrevWeek/onNextWeek/onCurrentWeek props,
  replacing the custom `header` slot. Nav buttons reuse the shared Button
  styled to match input/Button (light, bordered, Anderson font).
- Remove the unused dataIdFromObject.ts and its test; apollo.js already
  normalizes the cache via typePolicies, matching main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Changed button variant from "outline" to "default" for week navigation buttons to ensure consistent styling.
- Updated the border style of the navigation button class to use 'border-solid' for better visual clarity.
…tton

Remove classes verified to have no effect: box-border (global reset
already sets border-box), h-full on flex-stretched day columns, the
header's flex wrapper, and dead items-center/justify-center on the
non-flex shared button. Fold transition-all into the truncate hover
condition, replace the inline AndersonFont style with font-anderson,
and switch nav buttons to the ghost variant so NAV_BUTTON_CLASS no
longer overrides the default variant's colors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jerryzhou196 and others added 2 commits June 10, 2026 09:10
- Migrate swapPage imports to @apollo/client (post-main-merge)
- Drop COURSE_DROPDOWN_QUERY: the dropdown always has a term, so the
  term-scoped query is the only one needed (3 queries total now:
  sections by class numbers, sections by course code, course codes)
- Convert SectionFinderPanel to Tailwind and delete its
  styled-components file, as the migration starting point
- Revert DataUploadModals media-query additions to main's version

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
uwflow-frontend Ready Ready Preview, Comment Jun 13, 2026 7:26pm

lazyRetry.ts and ErrorFallback.tsx were committed by accident; nothing
imports them yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace non-null assertions with narrowed locals/guards, drop the
unused refetchAll prop plumbing, and remove an unused mixin import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jerryzhou196 and others added 3 commits June 10, 2026 10:24
Replaces the hand-rolled styled-components week grid in SwapCalendar with
components/calendar. Event cards now show a bold course code over a
'SECTION · LOCATION' line with variant-tinted fills, and the selected
course gets the gold full-border treatment. Page title is now
'Swap classes' with the existing subtitle beneath it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports ScheduleSwapPanel from the schedule-swap-panel branch into the swap
page, replacing SectionFinderPanel while keeping its Apollo data layer
(sections, conflicts, prof ratings). The panel now owns the term tabs and
the 'Swap X with Y' row; the swap target still uses the server-backed
CourseSearchDropdown, anchored under the panel's select-styled trigger.
'Switch section' pins the chosen section as a persistent calendar preview
(no enrollment mutation exists). Section-row previews use pointer events
(onPointerEnter/Leave) so they work with pen and touch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Uppercase muted day labels, 24-hour gutter times, fainter half-hour
lines, saturated accent left rails over pastel fills, and centered
three-line event content (course code lg, time · location, section sm).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread src/components/ui/button.tsx Outdated
Comment thread src/graphql/queries/course/SwapCourse.tsx Outdated
Comment thread src/pages/swapPage/styles/SwapCalendar.tsx Outdated
Comment thread src/pages/swapPage/styles/SwapPage.tsx Outdated
Comment thread src/pages/swapPage/CourseSearchDropdown.tsx
Addresses PR review: deletes both swapPage styles files - SwapPage's
wrapper/overlay/import-card and CourseSearchDropdown's dropdown styles
are converted to Tailwind, and the FadeInWrapper is replaced with the
animate-fade-in utility. The shared Button's border-none moves out of
the cva base into an @layer base button reset in src/index.css,
standing in for the disabled preflight (audited: no button relies on
the browser default border).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Removes the export button and its secretId/handleExport plumbing.
The slot is empty until the user makes a temporary swap, at which
point the reset button appears.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses PR review: COURSE_SECTION_FIELDS becomes a real gql fragment
(SwapCourseSection) and bun run generate emits the operation and
fragment types (purely additive, no schema drift). All hand-written
types in SwapCourse.tsx are deleted; consumers import generated types.
The fragment is a structural superset of the schedule entry's section,
so the temporary-swap bridge no longer needs a cast, and dead null
guards on non-null columns are gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stacked on #262 (merge that first). Second of three PRs matching the
swap page to the approved mockups.

- **Logged out** → the calendar renders a hardcoded, non-interactive
sample week (MATH 239, CS 240, STAT 231, CS 241, FINE 100 — negative
section ids, dated today so it always lands in the current term tab)
behind the existing frosted overlay, now containing a lock card: grey
lock circle, "**Upload your schedule to swap**", "Log in and paste your
courses from Quest to start swapping sections.", and a yellow **Log in
to continue** button that opens `AUTH_MODAL` via `useModal`.
- **Logged in without a schedule** → unchanged Quest-paste upload card.
- `SwapCalendar` gains a `demoMode` prop that disables block clicks; the
course-select trigger and section finder are already inert with no
selection, and Export stays hidden (no `secretId`).
- The logged-out "ephemeral parse" flow
(`GET_SECTIONS_BY_CLASS_NUMBERS`) lost its only entry point under this
design and has now been removed entirely (the linter flagged it as dead
code).

Verified with eslint (clean for `src/pages/swapPage`) and `tsc --noEmit`
(only the pre-existing `json-stable-stringify` config error). No
live-browser verification — please check `/swap` logged-out locally.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@edwinzhng

Copy link
Copy Markdown
Member

Can we align all the border radius with the rest of the app, move reset button inside the top section and remove the default button outline?

Also why is there an "X" button on the bottom panel? Doesn't seem like we should be able to close it

Color for "Conflicts" should probably match our error text which is more red? Why is it so close to the courses orange color

The action button for changing sections should be yellow like the rest of the CTAs in the app, and maybe "choose section" instead of "switch section"?

The dropdown for course selection is also not consistent with rest of the app, should only have orange text without the outline

Also not sure if I like the "Tag" format for the lecture name (Lec 001, etc.), should that just be the same title text format (bold + dark)? And probably just hide "Instructor TBA" entirely if there's no instructor, i thought we don't get that data anymore

- Align all border radii to the app-standard 4px (rounded)
- Move Reset inside the top swap bar as a borderless control
- Remove the close (X) button from the section panel; re-clicking the
  calendar block still deselects
- "Conflicts" label and 0-seat count use darkRed instead of red
- Section CTA is now the accent-yellow "Choose section" (new accent
  variant on ui/button)
- Course dropdown trigger is plain orange text, no box or focus ring
- Section names render as bold dark text instead of a tag badge
- Hide the instructor line entirely when there is no instructor data

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jerryzhou196

Copy link
Copy Markdown
Member Author

Addressed in f94a123:

  • Border radius — everything on the page now uses the app-standard 4px radius (same as the calendar blocks and Mixins cards).
  • Reset button — moved inside the top swap bar as a plain borderless text+icon control; the old outlined treatment is gone.
  • X button — removed from the bottom panel. Re-clicking the selected class on the calendar still deselects, so the panel was never dependent on it.
  • Conflicts color — switched from red (#ff5630, which does sit close to the courses orange) to darkRed (#de350b); the 0-seats count matches.
  • CTA — now the accent-yellow "Choose section" (added an accent variant to ui/button so other Tailwind CTAs can reuse it).
  • Course dropdown — trigger is now just orange text + chevron, no background box or focus ring.
  • Section names — dropped the tag/badge; "LEC 001" etc. render as bold dark title text (dimmed gray on conflicting rows).
  • Instructor TBA — the instructor line is hidden entirely when there's no prof data.

Replace the hand-rolled `button { border: none }` stand-in with
Preflight's `border-width: 0; border-style: solid`, so the `border`
utility alone draws the outline variant and no `border-solid` workaround
is needed. Addresses review feedback to move the button reset onto a
Tailwind preflight basis (full Preflight stays disabled to coexist with
the styled-components app).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/components/banner/AnnouncementBanner.tsx Outdated
Comment thread src/pages/swapPage/ScheduleSwapPanel.tsx
Comment thread src/pages/swapPage/SwapCalendar.tsx
Comment thread src/pages/swapPage/CourseSearchDropdown.tsx
…rmat

- Center AnnouncementBanner content (add justify-center, drop flex-1 so the
  icon/message/CTA/dismiss pack and center as a group)
- EmptyState placeholder icon RefreshCw -> MousePointer to match the
  "Click a class in your schedule" copy
- Add font-inter to the swap-bar dropdown trigger and Reset button: with
  Preflight disabled, public/index.css forces font-family: sans-serif on
  <button>, so the trigger rendered in a different font than its Inter
  siblings ("not aligned properly")
- Course dropdown rows render codes via formatCourseCode ("CS 135" not
  "CS135"), matching the rest of the app

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jerryzhou196 and others added 3 commits June 13, 2026 12:26
Direct/refreshed requests to client-side routes (e.g. /swap) had no
matching file or rewrite and returned 404. Add a catch-all rewrite to
/index.html, after the /graphql and /api proxies so those still win.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Capture term-schedule (Quest paste) upload failures via
Sentry.captureException in the UPLOAD_FAILED branch, tagged
feature: schedule_upload. Forwards HTTP status, the backend error
enum, any unmatched class numbers, and the exact pasted schedule
text so parse failures can be reproduced.

Also read the pasted value into a local instead of the stale
scheduleText state so the correct text is sent on each attempt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fadeIn keyframe was opacity-only over 0.2s, much subtler than the
app's <FadeIn> (react-fade-in), which animates opacity 0->1 plus a 20px
upward rise over 400ms. Match it: add the translateY(20px) rise and bump
the duration to 0.4s ease.

Move the fade off the SwapPage wrapper onto the SwapCalendar content
(which already uses animate-fade-in). A transform on the wrapper would
re-anchor the always-mounted fixed login/upload overlay, so the wrapper
keeps a plain background and the content carries the fade, mirroring how
the app's other pages wrap content in <FadeIn>.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

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.

Pull request overview

Adds a new Section Swap experience at /swap, providing a weekly schedule view with an interactive section browser (conflict detection, seat counts, prof ratings) and a course swap comparison workflow. This also advances the Tailwind migration by implementing the page UI in Tailwind and enhancing shared calendar styling to support the new UX.

Changes:

  • Introduces the /swap route and new swap page components (calendar view, section panel, course search dropdown).
  • Adds GraphQL queries/fragments to fetch swap-related section data and a course list per term.
  • Updates shared UI (Calendar styling/labels, announcement banner, profile dropdown nav) and Tailwind/base CSS to support the new page.

Reviewed changes

Copilot reviewed 21 out of 23 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
vercel.json Adds SPA rewrite fallback so /swap works on direct navigation.
tailwind.config.js Updates fadeIn keyframes/animation timing to match legacy page transitions.
src/types/Api.tsx Adds parse-only schedule response types for upload callback usage.
src/Routes.tsx Defines SWAP_PAGE_ROUTE.
src/pages/welcomePage/styles/WelcomePage.tsx Adds mobile padding adjustments on Welcome page wrapper.
src/pages/swapPage/SwapPage.tsx New /swap page: auth/schedule gating + upload/login overlay behavior.
src/pages/swapPage/SwapCalendar.tsx New swap calendar container: term switching, conflict detection, preview + temporary swap state.
src/pages/swapPage/ScheduleSwapPanel.tsx New section list panel: conflicts, seats, prof ratings, preview interactions, “choose section”.
src/pages/swapPage/demoSchedule.ts Demo schedule data for logged-out view.
src/pages/swapPage/CourseSearchDropdown.tsx New dropdown for searching courses to swap against (fuzzy search + virtualized list).
src/pages/profilePage/ProfileCalendar.tsx Aligns profile calendar time/day labels and link styling with swap page calendar.
src/LoadableComponents.tsx Adds lazy-loaded SwapPage entry.
src/index.css Adds minimal @layer base button border reset since Tailwind preflight is disabled.
src/graphql/queries/course/SwapCourse.tsx Adds swap-related fragments and queries (sections by course, sections by class #, course list by term).
src/generated/graphql.tsx Regenerates types/hooks for new swap queries/fragments.
src/components/upload/ScheduleUploadModalContent.tsx Enhances upload callback typing and adds Sentry reporting on upload failures.
src/components/ui/button.tsx Adds accent variant and documents border reset dependency.
src/components/navigation/ProfileDropdown.tsx Adds “Section swap” nav option to profile dropdown.
src/components/common/styles/LastUpdatedSchedule.tsx Adds optional fontSize styling support + inherit link font-size.
src/components/common/LastUpdatedSchedule.tsx Wires fontSize prop through to styled wrapper.
src/components/calendar/Calendar.tsx Updates calendar visuals (header height, labels, event rendering/states) to support swap UX.
src/components/banner/AnnouncementBanner.tsx Updates banner content to promote the new swapper and improves dismissal keying.
src/App.tsx Registers /swap route via SentryRoute + loadable SwapPage.

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

Comment on lines +131 to +137
extra: {
status,
error: (response as ErrorResponse).error,
failedClasses: (response as ScheduleParseResponse).failed_classes,
// The exact text the user pasted, so we can reproduce parse failures.
schedule: pastedSchedule,
},
Comment on lines 217 to 220
role={clickable ? 'button' : undefined}
tabIndex={clickable ? 0 : undefined}
onClick={clickable ? event.onClick : undefined}
style={{ top, height }}
Comment on lines +77 to +79
useEffect(() => {
inputRef.current?.focus();
}, []);
Comment on lines +43 to +57
const isEnrolled = false;
return (
<button
type="button"
style={style}
className={cn(
'flex w-full cursor-pointer items-center border-0 border-b border-solid border-light2 px-3.5 py-2.5 text-left text-[13px] text-dark1 last:border-b-0 hover:bg-light1',
isSelected
? 'bg-[#eef4ff]'
: isEnrolled
? 'bg-[#f5f8ff]'
: 'bg-transparent',
)}
onClick={() => onSelect(course.code)}
>
Comment on lines +68 to +70
const startHour = startSeconds / 3600;
if (startHour < GRID_START_HOUR || startHour > GRID_END_HOUR) return [];
return days.map((d) => DAY_LETTERS.indexOf(d)).filter((col) => col !== -1);
Comment on lines +13 to +15
padding: 16px;
padding-top: 16px;
box-sizing: border-box;
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.

4 participants