diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e592ec0cf..e821de09b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,10 +32,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index 65cab0156..bede01947 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Neon Database Create Branch Action uses: neondatabase/create-branch-action@v4 diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 40e8ebaf9..d1d7c6691 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -13,10 +13,10 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ea5cf7edc..f4ba5b717 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -70,7 +70,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python3.11 uses: actions/setup-python@v4 @@ -91,7 +91,7 @@ jobs: mongodb-port: 27017 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 @@ -111,10 +111,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 diff --git a/.github/workflows/validator_test.yml b/.github/workflows/validator_test.yml index 8f78c2e20..a410dc8b9 100644 --- a/.github/workflows/validator_test.yml +++ b/.github/workflows/validator_test.yml @@ -15,7 +15,7 @@ jobs: working-directory: validator steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: diff --git a/.github/workflows/versioning.yml b/.github/workflows/versioning.yml index 2989c45b1..906c851a9 100644 --- a/.github/workflows/versioning.yml +++ b/.github/workflows/versioning.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: diff --git a/src/components/planner/Sidebar/RecursiveRequirement.tsx b/src/components/planner/Sidebar/RecursiveRequirement.tsx index 163f96aa6..799415bd2 100644 --- a/src/components/planner/Sidebar/RecursiveRequirement.tsx +++ b/src/components/planner/Sidebar/RecursiveRequirement.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, Fragment } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { trpc } from '@/utils/trpc'; @@ -36,15 +36,14 @@ export function RecursiveRequirement({ > <> {req.requirements.map((req2, idx) => ( - <> +
Option {idx + 1}
- +
))} @@ -58,15 +57,13 @@ export function RecursiveRequirement({ filled={req.filled} > <> - {req.requirements.map((req2, idx) => ( - <> - - + {req.requirements.map((req2) => ( + ))} @@ -85,15 +82,14 @@ export function RecursiveRequirement({ > <> {req.requirements.map((req2, idx) => ( - <> +
Option {idx + 1}
- +
))} @@ -112,15 +108,14 @@ export function RecursiveRequirement({ > <> {req.requirements.map((req2, idx) => ( - <> +
Option {idx + 1}
- +
))} @@ -140,7 +135,7 @@ export function RecursiveRequirement({ <> {req.prefix_groups.map((req2, idx) => ( NOT SUPPORTED; } }; - return <>{getRequirement()}; + return getRequirement(); } function CourseRequirementComponent({ diff --git a/src/components/planner/Sidebar/RequirementsContainer.tsx b/src/components/planner/Sidebar/RequirementsContainer.tsx index b1cb2603e..b7593fce4 100644 --- a/src/components/planner/Sidebar/RequirementsContainer.tsx +++ b/src/components/planner/Sidebar/RequirementsContainer.tsx @@ -155,7 +155,9 @@ const getRequirementGroup = ( course: `${c.subject_prefix} ${c.course_number}`, matcher: 'Course', filled: courses.includes(`${c.subject_prefix} ${c.course_number}`), - metadata: {}, + metadata: { + id: `course-${c.id}`, + }, })) : [], filterFunction: filterFunc, @@ -200,12 +202,12 @@ const getRequirementGroup = ( }; export const ProgressComponent2 = ({ value, max }: { value: number; max: number }) => { - const heh = `${(value * 100) / max}%`; + const width = `${(value * 100) / max}%`; return (
-
+
); @@ -220,7 +222,7 @@ export const ProgressComponent = ({ max: number; unit?: string; }) => { - const heh = `${(value * 100) / max}%`; + const width = `${(value * 100) / max}%`; return (
@@ -228,7 +230,7 @@ export const ProgressComponent = ({ {value}/{max} {unit}
-
+
); diff --git a/src/components/planner/Sidebar/types.ts b/src/components/planner/Sidebar/types.ts index f1d82d381..a94b1c1a3 100644 --- a/src/components/planner/Sidebar/types.ts +++ b/src/components/planner/Sidebar/types.ts @@ -17,7 +17,7 @@ export type DegreeRequirement = Requirement & { export interface Requirement { matcher: string; - metadata: { [key: string]: string }; + metadata: { id: string; [key: string]: string }; filled: boolean; } @@ -36,7 +36,6 @@ export type AndRequirementGroup = Requirement & { export type HoursRequirementGroup = Requirement & { matcher: 'Hours'; - metadata: { [key: string]: string }; required_hours: number; fulfilled_hours: number; requirements: RequirementTypes[]; diff --git a/src/components/planner/Tiles/SemesterTile.tsx b/src/components/planner/Tiles/SemesterTile.tsx index 79f1cda7a..2a41c7e00 100644 --- a/src/components/planner/Tiles/SemesterTile.tsx +++ b/src/components/planner/Tiles/SemesterTile.tsx @@ -1,10 +1,11 @@ import { UniqueIdentifier, useDroppable } from '@dnd-kit/core'; -import React, { FC, forwardRef, useState, useRef } from 'react'; +import React, { FC, forwardRef, useState, useRef, useImperativeHandle } from 'react'; import AnalyticsWrapper from '@/components/common/AnalyticsWrapper'; import ChevronIcon from '@/icons/ChevronIcon'; import LockIcon from '@/icons/LockIcon'; import UnlockedIcon from '@/icons/UnlockedIcon'; +import useAccordionAnimation from '@/shared/useAccordionAnimation'; import { displaySemesterCode, getSemesterHourFromCourseCode } from '@/utils/utilFunctions'; import DraggableSemesterCourseItem from './SemesterCourseItem'; @@ -26,11 +27,17 @@ export interface SemesterTileProps { export const MemoizedSemesterTile = React.memo( forwardRef(function SemesterTile( { semester, getDragId }, - ref, + outerRef, ) { - const [open, setOpen] = useState(true); + const innerRef = useRef(null); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + useImperativeHandle(outerRef, () => innerRef.current!, []); + const [hoverOpen, setHoverOpen] = useState(false); const hoverTimer = useRef>(); + const { toggle, open } = useAccordionAnimation(innerRef, () => + semester.courses.length === 0 ? '140px' : '170px', + ); const { handleSelectCourses, @@ -96,8 +103,8 @@ export const MemoizedSemesterTile = React.memo( // QUESTION: isValid color? return (
@@ -110,7 +117,7 @@ export const MemoizedSemesterTile = React.memo( open ? '-rotate-90' : 'rotate-90' } ml-auto h-3 w-3 transform cursor-pointer text-neutral-500 transition-all duration-500`} fontSize="inherit" - onClick={() => setOpen(!open)} + onClick={toggle} />
@@ -153,8 +160,8 @@ export const MemoizedSemesterTile = React.memo( /> )}
{semester.courses.map((course) => ( diff --git a/src/components/planner/TransferBank.tsx b/src/components/planner/TransferBank.tsx index ad1c67844..03be8cd68 100644 --- a/src/components/planner/TransferBank.tsx +++ b/src/components/planner/TransferBank.tsx @@ -1,16 +1,21 @@ -import { FC, useState } from 'react'; +import { FC, useRef } from 'react'; import ChevronIcon from '@/icons/ChevronIcon'; +import useAccordionAnimation from '@/shared/useAccordionAnimation'; interface TransferBankProps { transferCredits: string[]; } const TransferBank: FC = ({ transferCredits }) => { - const [open, setOpen] = useState(true); + const bankRef = useRef(null); + const { toggle, open } = useAccordionAnimation(bankRef, () => '50px'); return ( -
+
Transfer Credits
= ({ transferCredits }) => { open ? '-rotate-90' : 'rotate-90' } h-3 w-3 transform cursor-pointer text-neutral-500 transition-all duration-500`} fontSize="inherit" - onClick={() => setOpen(!open)} + onClick={toggle} />
    {transferCredits.map((credit, i) => (
  1. void }) { } } } + // TODO: Consider whether credit was earned or not before adding to credits list const credits: Credit[] = []; let isTransfer = true; @@ -158,6 +159,7 @@ export default function CustomPlan({ onDismiss }: { onDismiss: () => void }) { } } } + const dedupedCredits = credits.reduce((acc, curr) => { if (!acc.some((i) => i.courseCode === curr.courseCode)) { acc.push(curr); diff --git a/src/shared/useAccordionAnimation.tsx b/src/shared/useAccordionAnimation.tsx new file mode 100644 index 000000000..ee06a8b7d --- /dev/null +++ b/src/shared/useAccordionAnimation.tsx @@ -0,0 +1,40 @@ +import { useState, useCallback } from 'react'; + +const useAccordionAnimation = ( + ref: React.RefObject | null, + getClosedHeight: () => string, +) => { + const [open, setOpen] = useState(true); + function collapseSection(element: HTMLElement, closedHeight: string) { + const sectionHeight = element.scrollHeight; + element.style.height = sectionHeight + 'px'; + setTimeout(() => { + element.style.height = closedHeight; + }, 100); + } + function expandSection(element: HTMLElement) { + const zero = performance.now(); + function animate() { + const value = (performance.now() - zero) / 700; + if (value < 1) { + element.style.height = element.scrollHeight + 8 + 'px'; + requestAnimationFrame(animate); + } + } + requestAnimationFrame(animate); + } + + const toggle = useCallback(() => { + if (!ref || !ref.current) return; + setOpen((prev) => !prev); + if (open) { + collapseSection(ref.current, getClosedHeight()); + } else { + expandSection(ref.current); + } + }, [open, ref, getClosedHeight]); + + return { toggle, open }; +}; + +export default useAccordionAnimation;