Skip to content

Commit

Permalink
feat: Tooltip
Browse files Browse the repository at this point in the history
  • Loading branch information
mathhulk committed Jun 10, 2024
1 parent e66da6b commit a9d95e2
Show file tree
Hide file tree
Showing 16 changed files with 383 additions and 316 deletions.
111 changes: 71 additions & 40 deletions frontend/src/app/Catalog/Class/Sections/Sections.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
padding: 24px;
text-align: center;

.title {
.heading {
color: var(--heading-color);
font-weight: 500;
margin-top: 24px;
}

.description {
.paragraph {
color: var(--paragraph-color);
margin-top: 8px;
max-width: 448px;
Expand All @@ -40,28 +40,32 @@
.view {
flex-grow: 1;

.section {
padding: 24px;
background-color: var(--foreground-color);
border-radius: 8px;
box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
margin-bottom: 24px;
border: 1px solid var(--border-color);

.header {
display: flex;
margin-bottom: 24px;
gap: 12px;

.text {
flex-grow: 1;
.group {
scroll-margin-top: 24px;

.title {
color: var(--heading-color);
font-size: 14px;
margin-bottom: 8px;
line-height: 1;
font-weight: 500;
.section {
padding: 24px;
background-color: var(--foreground-color);
border-radius: 8px;
box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
margin-bottom: 24px;
border: 1px solid var(--border-color);

.header {
display: flex;
margin-bottom: 24px;
gap: 12px;

.text {
flex-grow: 1;

.heading {
color: var(--heading-color);
font-size: 14px;
margin-bottom: 8px;
line-height: 1;
font-weight: 500;
}
}
}
}
Expand All @@ -70,8 +74,8 @@

.menu {
position: sticky;
top: 20px;
width: 128px;
top: 24px;
width: 192px;
flex-shrink: 0;
display: flex;
flex-direction: column;
Expand All @@ -82,28 +86,55 @@
border-radius: 4px;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
gap: 12px;
padding: 0 12px;
line-height: 1;
font-size: 14px;
font-weight: 500;
color: var(--paragraph-color);
width: fit-content;
cursor: pointer;

&:hover {
background-color: var(--slate-100);
background-color: var(--button-hover-color);

.component {
color: var(--heading-color);
}
}

&:active {
background-color: var(--button-active-color);
}

&:first-child {
&.active .component {
color: var(--heading-color);
position: relative;

&::after {
content: '';
width: 2px;
height: 100%;
background-color: var(--blue-500);
position: absolute;
left: -12px;
}
}

&.active::after {
opacity: 1;
}

&::after {
content: "";
width: 2px;
height: 100%;
background-color: var(--blue-500);
position: absolute;
opacity: 0;
left: -12px;
transition: all 100ms ease-in-out;
}

.component {
font-weight: 500;
color: var(--paragraph-color);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}

.count {
color: var(--label-color);
}
}
}
Expand Down
184 changes: 132 additions & 52 deletions frontend/src/app/Catalog/Class/Sections/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { useMemo } from "react";
import { useEffect, useMemo, useRef, useState } from "react";

import classNames from "classnames";
import { FrameAltEmpty, OpenNewWindow } from "iconoir-react";

import CCN from "@/components/CCN";
import Details from "@/components/Details";
import IconButton from "@/components/IconButton";
import LoadingIndicator from "@/components/LoadingIndicator";
import { IClass, Semester } from "@/lib/api";
import Tooltip from "@/components/Tooltip";
import { Component, IClass, Semester, components } from "@/lib/api";
import { getExternalLink } from "@/lib/section";

import Capacity from "../../../../components/Capacity";
Expand All @@ -23,71 +25,149 @@ export default function Sections({
currentYear,
currentSemester,
}: SectionsProps) {
const types = useMemo(
() =>
Array.from(
new Set(currentClass?.sections.map((section) => section.component))
),
[currentClass]
);
const viewRef = useRef<HTMLDivElement>(null);
const [group, setGroup] = useState<Component | null>(null);

const groups = useMemo(() => {
const sortedSections = structuredClone(currentClass?.sections ?? []).sort(
(a, b) => a.number.localeCompare(b.number)
);

return Object.groupBy(sortedSections, (section) => section.component);
}, [currentClass]);

useEffect(() => {
const element = viewRef.current;
if (!element) return;

let currentElement: HTMLElement | null = element;

while (currentElement) {
const overflowY = window.getComputedStyle(currentElement).overflowY;

if (overflowY === "auto") {
break;
}

currentElement = currentElement.parentElement;
}

if (!currentElement) return;

const updateGroup = () => {
const view = viewRef.current;
if (!view) return;

// element is a div that can scroll, find the child with the most pixels within the viewport
const children = Array.from(view.children) as HTMLElement[];

const visibleIndexes = children.map((child) => {
const rect = child.getBoundingClientRect();
const top = Math.max(rect.top, 0);
const bottom = Math.min(rect.bottom, window.innerHeight);

return bottom - top;
});

const maxVisibleIndex = visibleIndexes.reduce(
(maxIndex, visible, index) =>
visible > visibleIndexes[maxIndex] ? index : maxIndex,
0
);

const group = Object.keys(groups)[maxVisibleIndex] as Component;
setGroup(group);
};

updateGroup();

currentElement.addEventListener("scroll", updateGroup);

return () => {
currentElement.removeEventListener("scroll", updateGroup);
};
}, [groups]);

const handleClick = (index: number) => {
viewRef.current?.children[index].scrollIntoView({ behavior: "smooth" });
};

return currentClass ? (
currentClass.sections.length === 0 ? (
<div className={styles.placeholder}>
<FrameAltEmpty width={32} height={32} />
<p className={styles.title}>No associated sections</p>
<p className={styles.description}>
<p className={styles.heading}>No associated sections</p>
<p className={styles.paragraph}>
Please refer to the class syllabus or instructor for the most accurate
information regarding class attendance requirements.
</p>
</div>
) : (
<div className={styles.root}>
<div className={styles.menu}>
{types.map((type) => (
<div className={styles.item}>{type}</div>
{Object.keys(groups).map((component, index) => (
<div
className={classNames(styles.item, {
[styles.active]: group === component,
})}
onClick={() => handleClick(index)}
>
<p className={styles.component}>
{components[component as Component]}
</p>
<p className={styles.count}>
{groups[component as Component]?.length.toLocaleString()}
</p>
</div>
))}
</div>
<div className={styles.view}>
{currentClass.sections.map((section) => (
<div className={styles.section} key={section.ccn}>
<div className={styles.header}>
<div className={styles.text}>
<p className={styles.title}>
{section.component} {section.number}
</p>
<CCN ccn={section.ccn} />
</div>
<Capacity
enrollCount={section.enrollCount}
enrollMax={section.enrollMax}
waitlistCount={section.waitlistCount}
waitlistMax={section.waitlistMax}
/>
{currentClass && (
<IconButton
as="a"
href={getExternalLink(
currentYear,
currentSemester,
currentClass.course.subject,
currentClass.course.number,
section.number,
section.component
<div className={styles.view} ref={viewRef}>
{Object.values(groups).map((sections) => (
<div className={styles.group}>
{sections.map((section) => (
<div className={styles.section} key={section.ccn}>
<div className={styles.header}>
<div className={styles.text}>
<p className={styles.heading}>
{components[section.component]} {section.number}
</p>
<CCN ccn={section.ccn} />
</div>
<Capacity
enrollCount={section.enrollCount}
enrollMax={section.enrollMax}
waitlistCount={section.waitlistCount}
waitlistMax={section.waitlistMax}
/>
{currentClass && (
<Tooltip content="Open on Berkeley Academic Guide">
<a
href={getExternalLink(
currentYear,
currentSemester,
currentClass.course.subject,
currentClass.course.number,
section.number,
section.component
)}
target="_blank"
>
<IconButton>
<OpenNewWindow />
</IconButton>
</a>
</Tooltip>
)}
target="_blank"
>
<OpenNewWindow />
</IconButton>
)}
</div>
<Details
days={section.meetings[0].days}
startTime={section.meetings[0].startTime}
endTime={section.meetings[0].endTime}
location={section.meetings[0].location}
instructors={section.meetings[0].instructors}
/>
</div>
<Details
days={section.meetings[0].days}
startTime={section.meetings[0].startTime}
endTime={section.meetings[0].endTime}
location={section.meetings[0].location}
instructors={section.meetings[0].instructors}
/>
</div>
))}
</div>
))}
</div>
Expand Down
Loading

0 comments on commit a9d95e2

Please sign in to comment.