Link component, Card component, Course Popup component styling, and wrangling with the serialization type"
This commit is contained in:
@@ -6,7 +6,7 @@ import { CourseSchedule } from './CourseSchedule';
|
|||||||
* Also includes a link to their RateMyProfessor page
|
* Also includes a link to their RateMyProfessor page
|
||||||
*/
|
*/
|
||||||
export type Instructor = {
|
export type Instructor = {
|
||||||
name: string;
|
fullName: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
middleInitial?: string;
|
middleInitial?: string;
|
||||||
@@ -75,6 +75,32 @@ export class Course {
|
|||||||
constructor(course: Course | Serialized<Course>) {
|
constructor(course: Course | Serialized<Course>) {
|
||||||
Object.assign(this, course);
|
Object.assign(this, course);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getInstructorString(options: {
|
||||||
|
/** The maximum number of instructors to show */
|
||||||
|
max?: number;
|
||||||
|
format: 'abbr' | 'first_last' | 'last' | 'full_name';
|
||||||
|
}): string {
|
||||||
|
const { max = 3, format } = options;
|
||||||
|
|
||||||
|
if (!this.instructors) {
|
||||||
|
return 'Undecided';
|
||||||
|
}
|
||||||
|
|
||||||
|
const instructors = this.instructors.slice(0, max);
|
||||||
|
switch (format) {
|
||||||
|
case 'abbr':
|
||||||
|
return instructors.map(instructor => `${instructor.firstName?.[0]}. ${instructor.lastName}`).join(', ');
|
||||||
|
case 'full_name':
|
||||||
|
return instructors.map(instructor => instructor.fullName).join(', ');
|
||||||
|
case 'first_last':
|
||||||
|
return instructors.map(instructor => `${instructor.firstName} ${instructor.lastName}`).join(', ');
|
||||||
|
case 'last':
|
||||||
|
return instructors.map(instructor => instructor.lastName).join(', ');
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid Instructor String format: ${format}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
8
src/views/components/common/Card/Card.module.scss
Normal file
8
src/views/components/common/Card/Card.module.scss
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
@import 'src/views/styles/base.module.scss';
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: $white;
|
||||||
|
border: 1px solid #c3cee0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
27
src/views/components/common/Card/Card.tsx
Normal file
27
src/views/components/common/Card/Card.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import styles from './Card.module.scss';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
className?: string;
|
||||||
|
onClick?: (...args) => void;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
testId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reusable Card component that can be used to wrap other components
|
||||||
|
*/
|
||||||
|
export default function Card(props: Props) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={props.style}
|
||||||
|
className={classNames(styles.card, props.className)}
|
||||||
|
onClick={props.onClick}
|
||||||
|
data-testid={props.testId}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/views/components/common/Link/Link.module.scss
Normal file
5
src/views/components/common/Link/Link.module.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.link {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
25
src/views/components/common/Link/Link.tsx
Normal file
25
src/views/components/common/Link/Link.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { PropsWithChildren } from 'react';
|
||||||
|
import { bMessenger } from 'src/shared/messages';
|
||||||
|
import Text, { TextProps } from '../Text/Text';
|
||||||
|
import styles from './Link.module.scss';
|
||||||
|
|
||||||
|
type Props = TextProps & {
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reusable Text component with props that build on top of the design system for the extension
|
||||||
|
*/
|
||||||
|
export default function Link(props: PropsWithChildren<Props>) {
|
||||||
|
let passedProps = {
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
const { url } = props;
|
||||||
|
|
||||||
|
if (url && !props.onClick) {
|
||||||
|
passedProps.onClick = () => bMessenger.openNewTab({ url });
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Text {...passedProps} className={classNames(styles.link, props.className)} />;
|
||||||
|
}
|
||||||
@@ -17,16 +17,12 @@ export default function Popup(props: PropsWithChildren<Props>) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={props.style}
|
style={props.style}
|
||||||
className={classNames(
|
className={classNames(styles.container, {
|
||||||
styles.container,
|
|
||||||
{
|
|
||||||
[styles.overlay]: props.overlay,
|
[styles.overlay]: props.overlay,
|
||||||
},
|
})}
|
||||||
props.className
|
|
||||||
)}
|
|
||||||
data-testid={props.testId}
|
data-testid={props.testId}
|
||||||
>
|
>
|
||||||
<div className={styles.body}>{props.children}</div>
|
<div className={classNames(styles.body, props.className)}>{props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import colors, { ISassColors } from 'src/views/styles/colors.module.scss';
|
|||||||
import fonts, { Size, Weight } from 'src/views/styles/fonts.module.scss';
|
import fonts, { Size, Weight } from 'src/views/styles/fonts.module.scss';
|
||||||
import styles from './Text.module.scss';
|
import styles from './Text.module.scss';
|
||||||
|
|
||||||
type Props = {
|
export type TextProps = {
|
||||||
color?: keyof ISassColors;
|
color?: keyof ISassColors;
|
||||||
weight: Weight;
|
weight?: Weight;
|
||||||
size: Size;
|
size: Size;
|
||||||
span?: boolean;
|
span?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -18,7 +18,7 @@ type Props = {
|
|||||||
/**
|
/**
|
||||||
* A reusable Text component with props that build on top of the design system for the extension
|
* A reusable Text component with props that build on top of the design system for the extension
|
||||||
*/
|
*/
|
||||||
export default function Text(props: PropsWithChildren<Props>) {
|
export default function Text(props: PropsWithChildren<TextProps>) {
|
||||||
const style = props.style || {};
|
const style = props.style || {};
|
||||||
|
|
||||||
style.textAlign ??= props.align;
|
style.textAlign ??= props.align;
|
||||||
|
|||||||
@@ -1,35 +1,30 @@
|
|||||||
.popupBody {
|
.popup {
|
||||||
|
border-radius: 12px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.close {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
height: auto;
|
height: auto;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 10px;
|
padding: 12px;
|
||||||
|
margin: 40px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
|
||||||
|
|
||||||
.coursePopupBase {
|
.uniqueId {
|
||||||
position: fixed;
|
margin-left: 8px;
|
||||||
transform: translateY(-50%);
|
}
|
||||||
top: 15px;
|
}
|
||||||
right: 15px;
|
|
||||||
z-index: 2147483647;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coursePopupHeader {
|
|
||||||
display: flex;
|
|
||||||
height: 50;
|
|
||||||
background-color: #29465b;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.closePopupButton {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
margin: 5px;
|
|
||||||
margin-left: auto;
|
|
||||||
cursor: pointer;
|
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Course } from 'src/shared/types/Course';
|
import { Course } from 'src/shared/types/Course';
|
||||||
|
import Card from '../../common/Card/Card';
|
||||||
import Icon from '../../common/Icon/Icon';
|
import Icon from '../../common/Icon/Icon';
|
||||||
|
import Link from '../../common/Link/Link';
|
||||||
import Popup from '../../common/Popup/Popup';
|
import Popup from '../../common/Popup/Popup';
|
||||||
|
import Text from '../../common/Text/Text';
|
||||||
import styles from './CoursePopup.module.scss';
|
import styles from './CoursePopup.module.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -14,11 +17,24 @@ interface Props {
|
|||||||
*/
|
*/
|
||||||
export default function CoursePopup({ course, onClose }: Props) {
|
export default function CoursePopup({ course, onClose }: Props) {
|
||||||
return (
|
return (
|
||||||
<Popup overlay>
|
<Popup className={styles.popup} overlay>
|
||||||
<div className={styles.popupBody}>
|
<Icon className={styles.close} size='large' name='close' onClick={onClose} />
|
||||||
<div className={styles.courseTitle}>{course.courseName}</div>
|
<Card className={styles.body}>
|
||||||
<div className={styles.courseDescription}>{course.uniqueId}</div>
|
<Text className={styles.title} size='large' weight='bold' color='black'>
|
||||||
</div>
|
{course.courseName} ({course.department} {course.number})
|
||||||
|
<Link
|
||||||
|
span
|
||||||
|
url={course.url}
|
||||||
|
className={styles.uniqueId}
|
||||||
|
size='medium'
|
||||||
|
weight='semi_bold'
|
||||||
|
color='burnt_orange'
|
||||||
|
>
|
||||||
|
#{course.uniqueId}
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
</Card>
|
||||||
</Popup>
|
</Popup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,12 +159,12 @@ export class CourseCatalogScraper {
|
|||||||
.map(name => name.trim())
|
.map(name => name.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
return names.map(name => {
|
return names.map(fullName => {
|
||||||
const [lastName, rest] = name.split(',').map(s => s.trim());
|
const [lastName, rest] = fullName.split(',').map(s => s.trim());
|
||||||
const [firstName, middleInitial] = rest.split(' ');
|
const [firstName, middleInitial] = rest.split(' ');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
fullName,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
middleInitial,
|
middleInitial,
|
||||||
|
|||||||
Reference in New Issue
Block a user