Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"react-transition-group": "^4.4.2",
"react-virtualized": "^9.21.2",
"s-ago": "^2.2.0",
"swr": "^2.3.6",
"tslib": "2.3.0",
"use-local-storage-state": "^18.1.2",
"uuid": "^9.0.0"
Expand Down
2 changes: 1 addition & 1 deletion src/components/ActionRow/stylesheet.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
padding: 4px;

.action-row-header {
padding: 4px;
padding: 5px;
display: flex;
align-items: center;

Expand Down
2 changes: 2 additions & 0 deletions src/components/App/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { TooltipProvider } from 'react-tooltip';
import { Outlet } from 'react-router-dom';

import { classes } from '../../utils/misc';
import Feedback from '../Feedback';
Expand Down Expand Up @@ -66,6 +67,7 @@ export default function App(): React.ReactElement {
then it displays an error screen. */}
<AppDataLoader>
<AppContent />
<Outlet />
</AppDataLoader>
</AppNavigation>
<Feedback />
Expand Down
41 changes: 41 additions & 0 deletions src/components/Breadcrumb/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronRight } from '@fortawesome/free-solid-svg-icons';

import { classes } from '../../utils/misc';

import './stylesheet.scss';

export type BreadcrumbItem = {
label: string;
link?: string;
};

export type BreadcrumbProps = {
items: BreadcrumbItem[];
};

export default function Breadcrumb({
items,
}: BreadcrumbProps): React.ReactElement {
return (
<nav className={classes('Breadcrumb')}>
<ul className="list">
{items.map((item, index) => (
<li key={index} className="item">
{item.link ? (
<a href={item.link} className="link">
{item.label}
</a>
) : (
<span className="label">{item.label}</span>
)}
{index < items.length - 1 && (
<FontAwesomeIcon icon={faChevronRight} className="separator" />
)}
</li>
))}
</ul>
</nav>
);
}
34 changes: 34 additions & 0 deletions src/components/Breadcrumb/stylesheet.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.Breadcrumb {
display: flex;
align-items: center;
font-size: 16px;
padding: 8px 12px;
font-family: 'Roboto';

.list {
display: flex;
list-style: none;
margin: 0;
padding: 0;

.item {
display: flex;
align-items: center;

.link {
color: #8bd6fb;
text-decoration: none;
font-weight: 500;
}

.label {
font-weight: 500;
}

.separator {
margin: 0 6px;
font-size: 12px;
}
}
}
}
37 changes: 37 additions & 0 deletions src/components/MetricsCard/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';

import { classes } from '../../utils/misc';

import './stylesheet.scss';

export type Metric = {
label: string;
value: string;
unit?: string;
};

export type MetricsCardProps = {
metrics: Metric[];
};

export default function MetricsCard({
metrics,
}: MetricsCardProps): React.ReactElement {
return (
<div className={classes('MetricsCard')}>
<ul className="metrics-list">
{metrics.map((metric, index) => (
<li key={index} className="metric-item">
<div className="metric-value">
{metric.value}
{metric.unit && (
<span className="metric-unit"> {metric.unit}</span>
)}
</div>
<div className="metric-label">{metric.label}</div>
</li>
))}
</ul>
</div>
);
}
44 changes: 44 additions & 0 deletions src/components/MetricsCard/stylesheet.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
@import '../../variables.scss';

.MetricsCard {
display: flex;
padding: 16px;
border-radius: 8px;
font-family: 'Roboto', sans-serif;

.metrics-list {
display: flex;
list-style: none;
padding: 0;
margin: 0;

.metric-item {
display: flex;
flex-direction: column;
align-items: center;
margin-left: 40px;
padding-right: 40px;
border-right: 1px solid $color-border;

&:last-child {
border-right: none;
}

.metric-value {
font-size: 20px;
}

.metric-unit {
font-size: 14px;
font-weight: normal;
}

.metric-label {
font-size: 14px;
margin-top: 4px;
@include dark(color, $seat-info-dark-label);
@include light(color, $seat-info-light-label);
}
}
}
}
172 changes: 172 additions & 0 deletions src/components/ProfessorInfoCard/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import React, { useContext } from 'react';
import { faPlus, faCheck } from '@fortawesome/free-solid-svg-icons';
import useSWR from 'swr';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

import { ScheduleContext } from '../../contexts';
import { Course, Section } from '../../data/beans';
import ActionRow from '../ActionRow';
import { OccupiedInfo } from '../SeatInfo';

import './stylesheet.scss';

export type ProfessorInfoCardProps = {
professorName: string;
course: Course;
};

type SeatData = {
inClass: OccupiedInfo | null;
waitlist: OccupiedInfo | null;
};

const fetchSeating = async (
section: Section,
term: string
): Promise<SeatData> => {
try {
const raw = await section.fetchSeating(term);

// Handle missing or bad data, assuming less than 4 return values is invalid
if (!raw[0] || raw[0].length < 4) {
return { inClass: null, waitlist: null };
}

const [inClassTotal, inClassOccupied, waitlistTotal, waitlistOccupied] =
raw[0];

const toOccupiedInfo = (
total: unknown,
occupied: unknown
): OccupiedInfo => ({
occupied: Number(occupied ?? 0),
total: Number(total ?? 0),
});

return {
inClass: toOccupiedInfo(inClassTotal, inClassOccupied),
waitlist: toOccupiedInfo(waitlistTotal, waitlistOccupied),
};
} catch (err) {
return { inClass: null, waitlist: null };
}
};

function SectionRow({
section,
term,
isPinned,
onAdd,
}: {
section: Section;
term: string;
isPinned: boolean;
onAdd: () => void;
}): React.ReactElement {
const meeting = section.meetings[0];

const { data: seatData, isLoading } = useSWR<SeatData>(
['seating', section.crn, term],
() => fetchSeating(section, term)
);

const formatSeatData = (info: OccupiedInfo | null): string => {
if (isLoading) return 'Loading...';
if (!info) return 'N/A';
return `${info.occupied} / ${info.total}`;
};

return (
<ActionRow key={section.crn} className="section-row" label="" actions={[]}>
<div className="section-content">
<div className="section-cell action-cell">
<button
type="button"
className="action-button"
onClick={(): void => (isPinned ? undefined : onAdd())}
>
<FontAwesomeIcon
icon={isPinned ? faCheck : faPlus}
style={{
color: '#8BD6FB',
}}
/>
</button>
</div>

<div className="section-cell">{section.crn}</div>
<div className="section-cell">{section.id}</div>
<div className="section-cell">{meeting?.days.join('') || 'TBA'}</div>
<div className="section-cell">
{meeting?.period
? `${formatTime(meeting.period.start)}-${formatTime(
meeting.period.end
)}`
: 'TBA'}
</div>
<div className="section-cell">
{formatSeatData(seatData?.inClass ?? null)}
</div>
<div className="section-cell">
{formatSeatData(seatData?.waitlist ?? null)}
</div>
<div className="section-cell">{meeting?.where || 'TBA'}</div>
</div>
</ActionRow>
);
}

export default function ProfessorInfoCard({
professorName,
course,
}: ProfessorInfoCardProps): React.ReactElement {
const sections: Section[] = course.sections.filter((section: Section) => {
return section.instructors[0] === professorName;
});
const [{ pinnedCrns }, { patchSchedule }] = useContext(ScheduleContext);

const handleAddSection = (section: Section): void => {
patchSchedule({ pinnedCrns: [...pinnedCrns, section.crn] });
};

return (
<div className="ProfessorInfo">
<div className="professor-header">
<p className="professor-name">{professorName}</p>
</div>

<div className="sections-container">
<div className="sections-header">
<div className="header-cell" />
<div className="header-cell">CRN</div>
<div className="header-cell">Sect.</div>
<div className="header-cell">Day</div>
<div className="header-cell">Time</div>
<div className="header-cell">Seats Filled</div>
<div className="header-cell">Waitlist</div>
<div className="header-cell">Location</div>
</div>

{sections.map((section) => {
return (
<SectionRow
key={section.crn}
section={section}
term={course.term}
isPinned={pinnedCrns.includes(section.crn)}
onAdd={(): void => handleAddSection(section)}
/>
);
})}
</div>
</div>
);
}

function formatTime(minutes: number): string {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
const period = hours >= 12 ? 'pm' : 'am';
const displayHours = hours > 12 ? hours - 12 : hours === 0 ? 12 : hours;
return `${displayHours}:${mins.toString().padStart(2, '0')} ${period}`;
}
Loading
Loading