25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/components/icons/magnifying-glass.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/components/tests/pages/Home.test.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Search NEU and licensed under AGPL3.
3 | * See the license file in the root folder for details.
4 | */
5 |
6 | import React from 'react';
7 |
8 | import Enzyme, { shallow } from 'enzyme';
9 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
10 |
11 | import Home from '../../../pages/[campus]/[termId]';
12 |
13 | jest.mock('next/router', () => ({
14 | useRouter: () => ({ query: { campus: 'NEU' } }),
15 | }));
16 |
17 | jest.mock('use-query-params', () => ({
18 | useQueryParam: () => ['202030'],
19 | }));
20 |
21 | Enzyme.configure({ adapter: new Adapter() });
22 |
23 | it('should render a section', () => {
24 | const result = shallow();
25 | expect(result.debug()).toMatchSnapshot();
26 | });
27 |
--------------------------------------------------------------------------------
/docs/deployment/deployment.md:
--------------------------------------------------------------------------------
1 | # Git branches and deploying to production
2 |
3 | The master branch is the main branch for all the development. Merging into master deploys to searchneu.vercel.app. Releasing to searchneu.com must be done by merging `master` into the `prod` branch and pushing; this can only be done by someone on the team with admin privileges. Once changes in `prod` are pushed, Vercel will automatically deploy prod.
4 |
5 | If the frontend has new environment variables, they can be set by going to Vercel -> Settings -> Environment Variables.
6 |
7 | !> If any environment variable needs to be exposed to the browser (for example, the backend endpoint the browser hits), the variable should be prefixed with `NEXT_PUBLIC_`. See [Next.js environment variables](NEXT_PUBLIC_GRAPHQL_ENDPOINT).
8 |
--------------------------------------------------------------------------------
/components/Tooltip.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export enum TooltipDirection {
4 | Up = 'UP',
5 | Down = 'DOWN',
6 | }
7 |
8 | export type TooltipProps = {
9 | text: string;
10 | direction: TooltipDirection;
11 | /**
12 | * Decides the orientation of the Tooltip box; by default, the orientation has text
13 | * expanding on the right side. If this variable is set to T, the box will have
14 | * text expanding the left side, making it "flipped".
15 | */
16 | flipLeft?: boolean;
17 | };
18 |
19 | export default function Tooltip(props: TooltipProps) {
20 | return (
21 |
22 | {props.text}
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/styles/panels/_DesktopSectionPanel.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Search NEU and licensed under AGPL3.
3 | * See the license file in the root folder for details.
4 | */
5 | @use '../variables' as Colors;
6 |
7 | .wideOnlineCell {
8 | position: relative;
9 | }
10 |
11 | .onlineDivLineContainer {
12 | position: absolute;
13 | top: 0;
14 | left: 0;
15 | height: 100%;
16 | width: 100%;
17 | }
18 |
19 | .onlineDivLine {
20 | vertical-align: middle;
21 | display: inline-block;
22 | height: 2px;
23 | background-color: Colors.$Light_Grey;
24 | width: calc(50% - 102px);
25 | margin: 21px;
26 | }
27 |
28 | .onlineLeftLine {
29 | margin-left: 36px;
30 | }
31 |
32 | .inlineBlock {
33 | display: inline-block;
34 | }
35 |
36 | .sectionGlobe {
37 | opacity: 0.7;
38 | }
39 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: Bingbot
2 | User-agent: Googlebot
3 | User-Agent: Googlebot-Image
4 | User-Agent: Googlebot-Mobile
5 | User-agent: Mediapartners-Google
6 | User-agent: MSNbot
7 | User-agent: Slurp
8 | User-agent: Twitterbot
9 | User-agent: DuckDuckBot
10 | Disallow:
11 |
12 | # There's no point in allowing spiders to hit the api
13 | # It will just generate logs unnecessarily
14 | Disallow: /data
15 |
16 | #
17 | # Everyone else is banned. If you operate a search engine and would like to crawl Search NEU,
18 | # please contact hey@searchneu.com before crawling!
19 | # This was modeled after bluegolf.com/robots.txt
20 | #
21 |
22 | User-agent: *
23 | Disallow: /
24 |
25 | #
26 | # some specific bans
27 | #
28 | User-agent: Aboundexbot
29 | Disallow: /
30 |
31 | Sitemap: https://searchneu.com/sitemap.xml
32 |
--------------------------------------------------------------------------------
/components/ResultsPage/useAtTop.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Search NEU and licensed under AGPL3.
3 | * See the license file in the root folder for details.
4 | *
5 | * Little hook just to keep state on whether you're at the top of the screen
6 | */
7 |
8 | import { useState, useEffect } from 'react';
9 |
10 | export default function useAtTop(): boolean {
11 | const [atTop, setAtTop] = useState(true);
12 | useEffect(() => {
13 | const handleScroll = (): void => {
14 | const pageY =
15 | document.body.scrollTop || document.documentElement.scrollTop;
16 | setAtTop(pageY === 0);
17 | };
18 | window.addEventListener('scroll', handleScroll);
19 | handleScroll();
20 | return () => {
21 | window.removeEventListener('scroll', handleScroll);
22 | };
23 | }, []);
24 | return atTop;
25 | }
26 |
--------------------------------------------------------------------------------
/public/alert-banners.yml:
--------------------------------------------------------------------------------
1 | # This file is used to render banner alerts on the front page of search
2 | # To add an alert, add a new entry at the top level, preferrably with a descriptive name for the alert
3 | # Then, add key-value pair entries to that object, text, alertLevel, isVisible, and optional link. Example:
4 | # notificationsAlert:
5 | # text: "Due to a technical issue with our notification pathway through Facebook, notifications have been temporarily disabled. We're working on a new notification flow, and apologize for any inconvenience."
6 | # alertLevel: "error"
7 | # link: "https://www.w3schools.com"
8 | # For more info, check out the AlertBanner.tsx file
9 | ---
10 | notificationsAlert:
11 | text: 'New feature: SMS notifications are now available and have replaced Facebook Messenger notifications :D'
12 | alertLevel: 'info'
13 |
--------------------------------------------------------------------------------
/pages/api/classPage.graphql:
--------------------------------------------------------------------------------
1 | query getClassPageInfo($subject: String!, $classId: String!) {
2 | class(subject: $subject, classId: $classId) {
3 | name
4 | subject
5 | classId
6 | latestOccurrence {
7 | desc
8 | prereqs
9 | coreqs
10 | prereqsFor
11 | optPrereqsFor
12 | maxCredits
13 | minCredits
14 | classAttributes
15 | url
16 | prettyUrl
17 | lastUpdateTime
18 | feeAmount
19 | nupath
20 | host
21 | termId
22 | }
23 | allOccurrences {
24 | termId
25 | sections {
26 | classType
27 | crn
28 | seatsCapacity
29 | seatsRemaining
30 | waitCapacity
31 | waitRemaining
32 | campus
33 | profs
34 | meetings
35 | url
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/components/ResultsPage/Results/NotifSignUpButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react';
2 | import macros from '../../macros';
3 |
4 | interface NotifSignUpButtonProps {
5 | onNotifSignUp: () => void;
6 | }
7 |
8 | export default function NotifSignUpButton({
9 | onNotifSignUp,
10 | }: NotifSignUpButtonProps): ReactElement {
11 | const onClickWithAmplitudeHook = (): void => {
12 | onNotifSignUp();
13 | macros.logAmplitudeEvent('Notifs Button');
14 | };
15 |
16 | const NOTIFICATIONS_ARE_DISABLED = false;
17 |
18 | return (
19 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/components/Modal.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react';
2 |
3 | interface ModalProps {
4 | visible: boolean;
5 | onCancel: () => void;
6 | }
7 |
8 | export default function Modal({
9 | visible,
10 | onCancel,
11 | children,
12 | }: React.PropsWithChildren): ReactElement {
13 | React.useEffect(() => {
14 | const handleKeyEvent = (event: KeyboardEvent): void => {
15 | if (event.key === 'Escape') {
16 | onCancel();
17 | }
18 | };
19 |
20 | document.addEventListener('keydown', handleKeyEvent);
21 |
22 | return () => document.removeEventListener('keydown', handleKeyEvent);
23 | }, [onCancel]);
24 |
25 | return visible ? (
26 |
41 | ) : null;
42 | }
43 |
--------------------------------------------------------------------------------
/docs/dev-styles/style-guide.md:
--------------------------------------------------------------------------------
1 | # Commit Message Guidelines
2 |
3 | Here are the 7 steps for a good git commit message, as per [Chris Beam's blog](https://chris.beams.io/posts/git-commit/)
4 |
5 | 1. Separate subject from body with a blank line
6 | 2. Limit the subject line to 50 characters
7 | 3. Capitalize the subject line
8 | 4. Do not end the subject line with a period
9 | 5. Use the imperative mood in the subject line
10 | 6. Wrap the body at 72 characters
11 | 7. Use the body to explain what and why vs. how
12 |
13 | ### Examples:
14 |
15 | _Fix failing CompositePropertySourceTests_
16 | _Rework @PropertySource early parsing logic_
17 | _Add tests for ImportSelector meta-data_
18 | _Update docbook dependency and generate epub_
19 | _Polish mockito usage_
20 |
21 | # Squashing Pull Requests
22 |
23 | As a team, we've agreed to squash all of our pull requests for this project. The reason is because it will make our logs generally much cleaner and more concise. With the PR template, most code changes should be explained enough by the PR message, and smaller PR's will also keep this more in line.
24 |
--------------------------------------------------------------------------------
/components/panels/tests/EmployeePanel.test.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Search NEU and licensed under AGPL3.
3 | * See the license file in the root folder for details.
4 | */
5 |
6 | import React from 'react';
7 |
8 | import Enzyme, { shallow } from 'enzyme';
9 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
10 | import macros from '../../macros';
11 |
12 | import mockData from './mockData';
13 | import EmployeeResult from '../../ResultsPage/Results/EmployeeResult';
14 |
15 | Enzyme.configure({ adapter: new Adapter() });
16 |
17 | it('should render a desktop employee panel', () => {
18 | const orig = macros.isMobile;
19 | macros.isMobile = false;
20 |
21 | const result = shallow();
22 | expect(result.debug()).toMatchSnapshot();
23 |
24 | macros.isMobile = orig;
25 | });
26 |
27 | it('should render a mobile employee panel', () => {
28 | const orig = macros.isMobile;
29 | macros.isMobile = true;
30 |
31 | const result = shallow();
32 | expect(result.debug()).toMatchSnapshot();
33 |
34 | macros.isMobile = orig;
35 | });
36 |
--------------------------------------------------------------------------------
/components/ResultsPage/ToggleFilter.tsx:
--------------------------------------------------------------------------------
1 | import { uniqueId } from 'lodash';
2 | import React, { ChangeEvent, ReactElement, useState } from 'react';
3 |
4 | interface ToggleFilterProps {
5 | title: string;
6 | selected: boolean;
7 | setActive: (a: boolean) => void;
8 | }
9 |
10 | export default function ToggleFilter({
11 | title,
12 | selected,
13 | setActive,
14 | }: ToggleFilterProps): ReactElement {
15 | const [id] = useState(uniqueId('react-switch-'));
16 | const onChange = (event: ChangeEvent): void =>
17 | setActive(event.target.checked);
18 | return (
19 |
27 |
28 | Notifications have paused for the summer
29 |
30 |
31 | Due to cost issues, we{`'`}re putting notifications on pause until we acquire additional funding in the fall.
32 |
33 |
27 |
28 | It{`'`}s Giving Day!
29 |
30 |
31 | Make a donation today to Sandbox to help keep SearchNEU running!
32 |
33 |
34 | {
35 |
36 |
46 |
47 | }
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | Welcome to the SearchNEU documentation!
2 |
3 | ### What is SearchNEU?
4 |
5 | Banner — the site Northeastern uses for its course catalog — is .... less than ideal. It's tedious, requires a login, works poorly on mobile, can be difficult to search with, and has not the best UI/UX.
6 |
7 | SearchNEU, created by a student, was designed to help resolve some of these issues. It is a search engine built for easier navigation of class and professor information to help students with course registration. Users can search for and explore all class offerings within a semester, all faculty of the University, sections for each class, and other important information. Additionally, SearchNEU allows students to subscribe to notifications for a class with no remaining seats, to get notified when an opening appears in the class. All of our data is public information we scrape from Northeastern, so you can access any info with a quick search on searchneu.com.
8 |
9 | ### Tech Overview
10 |
11 | **"SearchNEU"**, as a complete application, exists in two parts:
12 |
13 | - Backend: The backend is our API server, which does all of the heavy lifting. This stores all of the course data - names, IDs, sections, descriptions, etc. It also handles notifications. The user can interact with this data using the frontend.
14 | - The backend is also used by other applications (like GraduateNU).
15 | - Frontend: The frontend is what a user sees when they go to [searchneu.com](https://searchneu.com). It does not have any data on its own - whenever a user searches for a course, the frontend sends a request to the backend, which returns the data. The frontend handles display; the backend handles data processing.
16 |
17 | This is the documentation for the **frontend**.
18 |
--------------------------------------------------------------------------------
/utils/QueryParamProvider.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import React, { memo, ReactElement, useMemo } from 'react';
3 | import { QueryParamProvider as ContextProvider } from 'use-query-params';
4 |
5 | export const QueryParamProviderComponent = (props: {
6 | children?: React.ReactNode;
7 | }): ReactElement => {
8 | const { children, ...rest } = props;
9 | const router = useRouter();
10 | const match = router.asPath.match(/[^?]+/);
11 | const pathname = match ? match[0] : router.asPath;
12 |
13 | const location = useMemo(
14 | () =>
15 | process.browser
16 | ? window.location
17 | : ({
18 | search: router.asPath.replace(/[^?]+/u, ''),
19 | } as Location),
20 | [router.asPath]
21 | );
22 |
23 | const history = useMemo(
24 | () => ({
25 | push: ({ search }: Location): Promise =>
26 | router.push(
27 | { pathname: router.pathname, query: router.query },
28 | { search, pathname },
29 | { shallow: true }
30 | ),
31 | replace: ({ search }: Location): Promise =>
32 | router.replace(
33 | { pathname: router.pathname, query: router.query },
34 | { search, pathname },
35 | { shallow: true }
36 | ),
37 | }),
38 | // yeah we need this since we don't no want reference equality
39 | // eslint-disable-next-line react-hooks/exhaustive-deps
40 | [pathname, router.pathname, router.query, location.pathname]
41 | );
42 |
43 | return (
44 |
45 | {children}
46 |
47 | );
48 | };
49 |
50 | export const QueryParamProvider = memo(QueryParamProviderComponent);
51 |
--------------------------------------------------------------------------------
/styles/pages/_Down.scss:
--------------------------------------------------------------------------------
1 | @use '../variables' as Colors;
2 | @use '../zIndexes' as Indexes;
3 | @import url('https://fonts.googleapis.com/css2?family=Montserrat+Alternates:wght@700&display=swap');
4 |
5 | /* Styles for Down Page. */
6 | .cryingHusky {
7 | width: 75% !important;
8 | height: 45% !important;
9 | position: absolute;
10 | right: -260px;
11 | bottom: 0px;
12 | z-index: Indexes.$Eight;
13 | }
14 |
15 | .huskyDollar {
16 | width: 75% !important;
17 | height: 45% !important;
18 | position: absolute;
19 | right: -90px;
20 | bottom: -150px;
21 | z-index: Indexes.$Eight;
22 | }
23 |
24 | .down-text-container {
25 | text-align: left;
26 | margin-left: 50px;
27 | margin-right: 50px;
28 |
29 | @media only screen and (min-width: 830px) {
30 | margin-left: 75px;
31 | margin-bottom: 200px;
32 | }
33 | }
34 |
35 | .down-title-text {
36 | font-family: Montserrat Alternates;
37 | color: Colors.$NEU_Red;
38 | font-size: 32px;
39 | font-weight: 800;
40 | margin-bottom: 20px;
41 |
42 | @media only screen and (min-width: 830px) {
43 | font-size: 55px;
44 | margin-bottom: 30px;
45 | }
46 | }
47 |
48 | .down-sub-title-text {
49 | font-family: Lato;
50 | font-size: 20px;
51 | font-weight: 600;
52 | color: Colors.$Navy;
53 | margin-bottom: 20px;
54 | margin-right: 20px;
55 |
56 | @media only screen and (min-width: 830px) {
57 | font-size: 25px;
58 | margin-bottom: 40px;
59 | }
60 | }
61 |
62 | .down-text {
63 | font-family: Lato;
64 | font-size: 16px;
65 | font-weight: 400;
66 | color: Colors.$Navy;
67 | width: 85%;
68 | white-space: pre-line;
69 |
70 | @media only screen and (min-width: 830px) {
71 | height: 112px;
72 | width: 45%;
73 | margin-right: 20px;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/components/tests/pages/__snapshots__/Home.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should render a section 1`] = `
4 | "
5 |
42 | "
43 | `;
44 |
--------------------------------------------------------------------------------
/utils/useUserInfo.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { UserInfo } from '../components/types';
3 | import Cookies from 'universal-cookie';
4 | import axios from 'axios';
5 |
6 | type useUserInfoReturn = {
7 | userInfo: UserInfo | null;
8 | isUserInfoLoading: boolean;
9 | fetchUserInfo: () => void;
10 | onSignOut: () => void;
11 | onSignIn: (token: string) => void;
12 | };
13 |
14 | // Custom hook to maintain all userInfo related utility functions
15 | const useUserInfo = (): useUserInfoReturn => {
16 | const cookies = new Cookies();
17 | const [userInfo, setUserInfo] = useState(null);
18 | const [isUserInfoLoading, setIsUserInfoLoading] = useState(true);
19 |
20 | const onSignOut = (): void => {
21 | cookies.remove('SearchNEU JWT', { path: '/' });
22 | setUserInfo(null);
23 | };
24 |
25 | const onSignIn = (token: string): void => {
26 | cookies.set('SearchNEU JWT', token, { path: '/' });
27 | fetchUserInfo();
28 | };
29 |
30 | const fetchUserInfo = async (): Promise => {
31 | const token = cookies.get('SearchNEU JWT');
32 | if (token) {
33 | await axios
34 | .get(
35 | `${process.env.NEXT_PUBLIC_NOTIFS_ENDPOINT}/user/subscriptions/${token}`
36 | )
37 | .then(({ data }) => {
38 | setUserInfo({ token, ...data });
39 | })
40 | .catch((e) => {
41 | console.log(e);
42 | });
43 | }
44 | };
45 |
46 | useEffect(() => {
47 | const fetchData = async (): Promise => {
48 | await fetchUserInfo();
49 | setIsUserInfoLoading(false);
50 | };
51 | fetchData();
52 | }, []);
53 |
54 | return { userInfo, isUserInfoLoading, fetchUserInfo, onSignOut, onSignIn };
55 | };
56 |
57 | export default useUserInfo;
58 |
--------------------------------------------------------------------------------
/docs/styling-css/styling.md:
--------------------------------------------------------------------------------
1 | # Styling
2 |
3 | SearchNEU is powered by Sass. Sass is a superscript of CSS that provides some really nice features that help us with keeping our styles consistent and distinct.
4 |
5 | If you're unfamiliar with Sass, I'd highly recommend checking out their [getting started page][sass-getting-started].
6 |
7 | ## Layout
8 |
9 | All our styling files are in `/styles`, and are parallel to the content in `/components`. Partials, `.scss` files whose first character is an underscore `_`, should never affect styles outside that specific file. To resolve this, we must namespace and ecapsulate our styles. Practically, this means that every partial should have one root style, and all styling is nested within that style.
10 |
11 | ## Exceptions
12 |
13 | There are three exceptions to the above layout, `css/base.scss`, `_variables.scss`, and `_zIndexes.css`. The variables partial keeps track of all our common themes and colors. This file is imported first, so all following partials can use any variables.
14 |
15 | `base.scss` itself has two purposes, 1) to import any partial styles, and 2) normalizing styles. This file should not contain anything else.
16 |
17 | ## Z-Indexes
18 |
19 | In addition to utilizing variables for themes and colors, we use variables for our Z-indexes as well. Located in `/styles` in the `_zIndexes.scss` partial, our Z-index levels range from 1-15 and should be used instead of raw index numbers.
20 |
21 | ## Adding new styles
22 |
23 | Adding a new style file is easy. First, create a Sass file that reflects the file path relative to the `components` folder. Make sure it's a partial (`_.scss`). Next, import it in `base.scss`. Finally, make sure you namespace that file. We do not like having leaking styles! :c
24 |
25 | [sass-getting-started]: http://sass-lang.com/guide
26 |
--------------------------------------------------------------------------------
/components/icons/NavArrow.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react';
2 | import Colors from '../../styles/_exports.module.scss';
3 |
4 | export const LeftNavArrow = ({
5 | width = '9',
6 | height = '20',
7 | fill = Colors.black,
8 | className,
9 | }: {
10 | width?: string;
11 | height?: string;
12 | fill?: string;
13 | className?: string;
14 | }): ReactElement => (
15 |
28 | );
29 |
30 | export const RightNavArrow = ({
31 | width = '9',
32 | height = '20',
33 | fill = Colors.black,
34 | className,
35 | }: {
36 | width?: string;
37 | height?: string;
38 | fill?: string;
39 | className?: string;
40 | }): ReactElement => (
41 |
54 | );
55 |
--------------------------------------------------------------------------------
/components/Testimonial/TestimonialModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react';
2 | import Modal from '../Modal';
3 | import X from '../icons/X.svg';
4 | import CryingHusky3 from '../icons/crying-husky-3.svg';
5 |
6 | interface TestimonialModalProps {
7 | visible: boolean;
8 | onCancel: () => void;
9 | }
10 |
11 | export default function TestimonialModal({
12 | visible,
13 | onCancel,
14 | }: TestimonialModalProps): ReactElement {
15 | return (
16 |
17 |
18 |
19 |
20 |
26 |
27 |
28 | We need your help!
29 |
30 |
31 | Share your testimonial about how we{`'`}ve helped you. Your feedback
32 | is valuable to us!
33 |
34 |
35 |
36 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/components/notifications/SignUpForSectionNotifications.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Search NEU and licensed under AGPL3.
3 | * See the license file in the root folder for details.
4 | */
5 |
6 | import React, { ReactElement } from 'react';
7 | import { UserInfo } from '../types';
8 | import CourseCheckBox from '../panels/CourseCheckBox';
9 | import { Course } from '../types';
10 |
11 | type SignUpForSectionNotificationsProps = {
12 | course: Course;
13 | userInfo: UserInfo;
14 | showNotificationSignup: boolean;
15 | fetchUserInfo: () => void;
16 | onSignIn: (token: string) => void;
17 | };
18 |
19 | export default function SignUpForSectionNotifications({
20 | course,
21 | userInfo,
22 | showNotificationSignup,
23 | fetchUserInfo,
24 | onSignIn,
25 | }: SignUpForSectionNotificationsProps): ReactElement {
26 | const numOpenSections = course.sections.reduce((prev, cur) => {
27 | if (cur.seatsRemaining > 0) {
28 | return prev + 1;
29 | }
30 | return prev;
31 | }, 0);
32 |
33 | const openSectionsText =
34 | numOpenSections === 1
35 | ? 'There is 1 section with seats left.'
36 | : `There are ${numOpenSections} sections with seats left.`;
37 |
38 | return showNotificationSignup ? (
39 | userInfo ? (
40 |
41 |
42 | Notify me when new sections are added:
43 |
44 |
50 |
51 | ) : (
52 | // Need to replace this once mobile notifs are finalized
53 | <>Sign in for new section notifications.>
54 | )
55 | ) : (
56 |
34 | Sign in for notifications
35 |
36 |
37 | Your phone number will be used for class notifications and nothing
38 | else.
39 |
40 |
41 |
47 | {error && {error}}
48 |
49 |
61 |
64 |
65 |
66 | >
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/components/icons/IconTie.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react';
2 |
3 | /* eslint-disable no-restricted-syntax */
4 |
5 | const IconTie = ({
6 | width = '24',
7 | height = '21',
8 | className,
9 | }: {
10 | width?: string;
11 | height?: string;
12 | className?: string;
13 | }): ReactElement => (
14 |
54 | );
55 |
56 | export default IconTie;
57 |
--------------------------------------------------------------------------------
/docs/setup/development.md:
--------------------------------------------------------------------------------
1 | ## Front-end only
2 |
3 | If the GraphQL endpoint exposed by production `course-catalog-api` supports the query you want to make, you just have to:
4 |
5 | 1. Create a `.graphql` file somewhere in the project (it can be anywhere but we'd suggest the in `pages/api`) and write your GraphQL query (or edit an existing `.graphql` file to modify an existing query).
6 | 2. Run `yarn generate:graphql` to auto-generate types -- this will hit the production endpoint `https://api.searchneu.com`
7 | - running `yarn dev` also works since it runs the `generate:graphql` command
8 | 3. In your code, you can use `gqlClient` from `utils/courseAPIClient` to make the GraphQL query by doing something like
9 | `await gqlClient.whateverYouNamedYourQuery()`
10 | 4. Start up SearchNEU locally by running `yarn dev`
11 |
12 | ## Full-stack
13 |
14 | If the GraphQL endpoint exposed by production `course-catalog-api` does NOT support the query you want to make, you have to:
15 |
16 | 1. Make changes to your local version of `course-catalog-api` so it supports your query
17 | 2. Start up `course-catalog-api` locally (it should run at `localhost:4000`)
18 | 3. Steps 1 and 3 from the front-end only flow are the same
19 | 4. Run `yarn generate:graphql:fullstack` to auto-generate types -- this will hit your local GraphQL endpoint which is `http://localhost:4000`. If it's not up, you'll get an error.
20 | - running `yarn dev:fullstack` also works since it will run `generate:graphql` with the local GraphQL endpoint
21 | 5. Start up SearchNEU locally by running `yarn dev:fullstack`
22 |
23 | # Why does it work this way?
24 |
25 | The GraphQL endpoint is currently used in 2 places in the code:
26 |
27 | - In `codegen.yml` so when we run `yarn generate:graphql`, the codegen knows what endpoint to hit to generate types
28 | - In `courseAPIClient.ts` so the `gqlClient` used in other places of our code knows what endpoint to hit when making GraphQL queries
29 |
30 | This endpoint is an environment variable that we set in `.env.development`. By default, we have it pointing to our production endpoint `https://api.searchneu.com`. We can override this endpoint with `http://localhost:4000` by setting `NEXT_PUBLIC_GRAPHQL_ENDPOINT=http://localhost:4000` when we run commands like `yarn dev` and `yarn generate:graphql`. In fact, this is exactly what `yarn dev:fullstack` and `yarn generate:graphql:fullstack` do!
31 |
--------------------------------------------------------------------------------
/components/ClassPage/PageContent.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import React, { ReactElement } from 'react';
3 | import { GetClassPageInfoQuery } from '../../generated/graphql';
4 | import ClassPageInfoBody from './ClassPageInfoBody';
5 | import ClassPageInfoHeader from './ClassPageInfoHeader';
6 | import ClassPageReqsBody from './ClassPageReqsBody';
7 | import ClassPageSections from './ClassPageSections';
8 |
9 | type PageContentProps = {
10 | termId: string;
11 | campus: string;
12 | subject: string;
13 | classId: string;
14 | classPageInfo: GetClassPageInfoQuery;
15 | isCoreq: boolean;
16 | };
17 |
18 | export default function PageContent({
19 | termId,
20 | campus,
21 | subject,
22 | classId,
23 | classPageInfo,
24 | isCoreq,
25 | }: PageContentProps): ReactElement {
26 | const router = useRouter();
27 |
28 | // TODO: hacky front-end solution because for some reason allOccurrences includes
29 | // termIds where there are no sections. This should probably be fixed on the backend.
30 | if (classPageInfo && classPageInfo.class) {
31 | classPageInfo.class.allOccurrences = classPageInfo.class.allOccurrences.filter(
32 | (occurrence) => occurrence.sections.length > 0
33 | );
34 | }
35 | return (
36 |
37 | {isCoreq ? (
38 |
39 | COREQUISITES for
40 | {` ${subject}${classId}`}
41 |
72 | );
73 | }
74 |
75 | export default React.memo(SemesterDropdown);
76 |
--------------------------------------------------------------------------------
/docs/setup/setup.md:
--------------------------------------------------------------------------------
1 | ### Installing the dependencies
2 |
3 | Almost every Node.js project has a lot of dependencies. These include React, Lodash, Webpack, and usually a bunch of other libraries. Lets install them.
4 |
5 | ```bash
6 | yarn
7 | ```
8 |
9 | If you get installation errors, try deleting the `node_modules` folder and running the install command again.
10 |
11 | ### Start the server
12 |
13 | This will start Search NEU in development mode locally. It will listen on port 5000. If you make any changes to the frontend code while the server is running, webpack will automatically recompile the code and send the updates to the browser. Most of the time, the changes should appear in the browser without needing to reload the page ([More info about Hot Module Reloading](https://webpack.js.org/concepts/hot-module-replacement/)). Sometimes this will fail and a message will appear in Chrome's developer tools asking you to reload the page to see the changes.
14 |
15 | ```bash
16 | yarn dev
17 | ```
18 |
19 | ### React Dev tools
20 |
21 | Also, install the React Developer tools browser extension ([Chrome](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en), [Firefox](https://addons.mozilla.org/en-US/firefox/addon/react-devtools/)). This helps a lot with debugging the frontend React code. More about debugging below.
22 |
23 | ### Debugging
24 |
25 | Chrome dev tools are great for debugging both Node.js code and JavaScript code in a browser. You can debug a Node.js script by running `babel-node` (or `node`) with these arguments:
26 |
27 | ```bash
28 | babel-node --debug --inspect-brk filename.js
29 | ```
30 |
31 | ### Run the tests
32 |
33 | ```bash
34 | # Run the tests once and exit
35 | yarn test
36 |
37 | # Run just the files that have changed since the last git commit
38 | yarn test --watch
39 |
40 | # Run all the tests
41 | yarn test --watchAll
42 |
43 | # Run all the tests and generate a code coverage report.
44 | # An overview is shown in the termal and a more detailed report is saved in the coverage directory.
45 | yarn jest --coverage --watchAll
46 | ```
47 |
48 | ### Build the code for production
49 |
50 | This command will build the frontend.
51 |
52 | ```bash
53 | yarn build
54 | ```
55 |
56 | ### Linting
57 |
58 | Some of the code follows the ESLint config. All the code in the codebase should pass these linting checks.
59 |
60 | ```bash
61 | yarn lint
62 | ```
63 |
64 | Prettier formats code automatically when you git commit, so don't waste time manual formatting.
65 |
--------------------------------------------------------------------------------
/components/ResultsPage/MobileSearchOverlay.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react';
2 | import IconClose from '../icons/IconClose';
3 | import macros from '../macros';
4 | import FilterPanel from './FilterPanel';
5 | import FilterPills from './FilterPills';
6 | import { areFiltersSet, FilterOptions, FilterSelection } from './filters';
7 | import Colors from '../../styles/_exports.module.scss';
8 |
9 | /**
10 | * setFilterPills sets the selected filters
11 | * onExecute indicates the query should be run and we should return to the results page
12 | * onClose indicates the user wants to close the overlay and return to wherever we were before
13 | * filterSelection is the list of selected filters
14 | * filterOptions is the available options for the filters
15 | * query is the search query
16 | */
17 | interface MobileSearchOverlayProps {
18 | setFilterPills: (f: FilterSelection) => void;
19 | onExecute: () => void;
20 | filterSelection: FilterSelection;
21 | filterOptions: FilterOptions;
22 | }
23 |
24 | export default function MobileSearchOverlay({
25 | setFilterPills,
26 | filterSelection,
27 | filterOptions,
28 | onExecute,
29 | }: MobileSearchOverlayProps): ReactElement {
30 | // Hide keyboard and execute search
31 | const search = (): void => {
32 | if (macros.isMobile) {
33 | if (
34 | document.activeElement &&
35 | document.activeElement instanceof HTMLElement
36 | ) {
37 | document.activeElement.blur();
38 | }
39 | }
40 | onExecute();
41 | };
42 | return (
43 |