Skip to content

Commit

Permalink
Merge branch 'master' into jacob/fix-search-scroll-reset
Browse files Browse the repository at this point in the history
  • Loading branch information
js0mmer authored Nov 4, 2023
2 parents 5e1d961 + 446ea15 commit dd744f6
Show file tree
Hide file tree
Showing 32 changed files with 502 additions and 185 deletions.
4 changes: 4 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@
(optional)
- [ ] Write tests
- [ ] Write documentation

## Issues
<!-- Link the issue you're closing -->
Closes #
1 change: 1 addition & 0 deletions .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jobs:
PRODUCTION_DOMAIN: ${{secrets.PRODUCTION_DOMAIN}}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
NODE_ENV: ${{ github.event_name == 'pull_request' && 'staging' || 'production' }}

- name: Comment staging URL
uses: marocchino/sticky-pull-request-comment@v2
Expand Down
74 changes: 65 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
![petr](https://github.com/icssc-projects/peterportal-public-api/blob/master/public/images/peterportal-banner-logo.png?raw=true)

PeterPortal is a web application aimed to aid UCI students with course discovery. We consolidate public data available on multiple UCI sources on the application to improve the user experience when planning their course schedule.

🔨 Built with:

* PeterPortal API
* ExpressJS
* ReactJS
## About

PeterPortal is a web application aimed to aid UCI students with course discovery and planning. We consolidate public data available on multiple UCI sources on the application to improve the user experience when planning their course schedule.

Features include:
* Course catalog with:
* Grade distribution graphs/charts
* Visual prerequisite trees
* Schedule of classes
* Reviews
![catalogue](https://github.com/icssc/peterportal-client/assets/8922227/e2e34103-a73e-4fd9-af44-69b707d1e910)
![coursepage](https://github.com/icssc/peterportal-client/assets/8922227/2df5a284-0040-4720-a9be-c08978b6bfb1)
* Professor catalog with:
* Schedule of classes
* Grade distribution graphs/charts
* Reviews
* Peter's Roadmap, a drag-and-drop 4-year course planner

![roadmap](https://github.com/icssc/peterportal-client/assets/8922227/7849f059-ebb6-43b4-814d-75fb850fec01)

## 🔨 Built with

* [PeterPortal API](https://github.com/icssc/peterportal-api-next)
* Express
* React
* SST and AWS CDK
* MongoDB
* GraphQL
* Typescript
* TypeScript

## First time setup
### Committee Members
1. Clone the repository to your local machine:
```
git clone https://github.com/icssc/peterportal-client
Expand All @@ -29,6 +48,43 @@ PeterPortal is a web application aimed to aid UCI students with course discovery
5. Run `npm install` to install all node dependencies for the site and API. This may take a few minutes.
6. Setup the appropriate environment variables provided by the project lead.
### Open Source Contributors
1. Fork the project by clicking the fork button in the top right, above the about section.
2. Clone your forked repository to your local machine
```
git clone https://github.com/<your username>/peterportal-client
```
3. Switch to a branch you will be working on.
```
git checkout -b [branch name]
```
4. Check your Node version with `node -v`. Make sure you have version 14 or above (18 recommended).
5. Open a terminal window and `cd` into the directory of your repo.
6. Run `npm install` to install all node dependencies for the site and API. This may take a few minutes.
7. Create a .env file in the api directory with the following contents:
```
PUBLIC_API_URL=https://api.peterportal.org/rest/v0/
PUBLIC_API_GRAPHQL_URL=https://api.peterportal.org/graphql/
PORT=5000
```
Note: the port should also match the one set up on the frontend's proxy to the backend under `site/src/setupProxy.js` By default this is 5000.
8. (Optional) Set up your own MongoDB and Google OAuth to be able to test features that require signing in such as leaving reviews or saving roadmaps to your account. Add additional variables/secrets to the .env file from the previous step.
```
MONGO_URL=<secret>
SESSION_SECRET=<secret>
GOOGLE_CLIENT=<client>
GOOGLE_SECRET=<secret>
ADMIN_EMAILS=["<your email>"]
```
## Running the project locally (after setup)
1. Open two terminal windows and `cd` into the directory of your repo in each of them.
Expand All @@ -41,7 +97,7 @@ PeterPortal is a web application aimed to aid UCI students with course discovery
## Where does the data come from?
We consolidate our data directly from official UCI sources such as: UCI Catalogue, UCI Public Records Office, and UCI Webreg (courtesy of [PeterPortal API](https://github.com/icssc/peterportal-api-nex)).
We consolidate our data directly from official UCI sources such as: UCI Catalogue, UCI Public Records Office, and UCI WebReg (courtesy of [PeterPortal API](https://github.com/icssc/peterportal-api-next)).
## Bug Report
🐞 If you encountered any issues or bug, please open an issue @ https://github.com/icssc-projects/peterportal-client/issues/new
Expand Down
14 changes: 12 additions & 2 deletions api/src/controllers/reviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,17 @@ router.post("/", async function (req, res, next) {
* Delete a review
*/
router.delete('/', async (req, res, next) => {
if (req.session.passport?.admin) {

const checkUser = async () => {

let review = await getDocuments(COLLECTION_NAMES.REVIEWS, {
_id: new ObjectID(req.body.id)
});

return review.length > 0 && review[0].userID === req.session.passport?.user.id;
}

if (req.session.passport?.admin || await checkUser()) {
console.log(`Deleting review ${req.body.id}`);

let status = await deleteDocument(COLLECTION_NAMES.REVIEWS, {
Expand All @@ -179,7 +189,7 @@ router.delete('/', async (req, res, next) => {
res.json(status);
}
else {
res.json({ error: 'Must be an admin to delete reviews!' });
res.json({ error: 'Must be an admin or review author to delete reviews!' });
}
})

Expand Down
2 changes: 1 addition & 1 deletion api/src/helpers/mongo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const client = new MongoClient(process.env.MONGO_URL, { useNewUrlParser: true, u
/**
* Database name to use in mongo
*/
const DB_NAME = 'peterPortalDB';
const DB_NAME = process.env.NODE_ENV == 'production' ? 'peterPortalDB' : 'peterPortalDevDB';
/**
* Collection names that we are using
*/
Expand Down
2 changes: 1 addition & 1 deletion api/src/types/environment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ declare global {
*/
interface ProcessEnv {
MONGO_URL: string;
NODE_ENV: 'development' | 'production';
NODE_ENV: 'development' | 'production' | 'staging';
PORT?: string;
PUBLIC_API_URL: string;
PUBLIC_API_GRAPHQL_URL: string;
Expand Down
2 changes: 1 addition & 1 deletion site/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<link href="https://fonts.googleapis.com/css?family=Open+Sans&display=swap" rel="stylesheet" />

<meta name="theme-color" content="#000000" />
<meta name="description" content="Enrolling in classes can now be stress-free and organized with PeterPortal." />
<meta name="description" content="A web application for course discovery and planning at UCI, featuring an enhanced catalogue and a 4-year planner." />
<title>PeterPortal</title>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-R44HQTN9E1"></script>
Expand Down
Binary file removed site/public/searching.png
Binary file not shown.
2 changes: 2 additions & 0 deletions site/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ErrorPage from './pages/ErrorPage';
import RoadmapPage from './pages/RoadmapPage';
import ZotisticsPage from './pages/ZotisticsPage';
import AdminPage from './pages/AdminPage';
import ReviewsPage from './pages/ReviewsPage';
import SideBar from './component/SideBar/SideBar';

import { useAppSelector } from './store/hooks';
Expand Down Expand Up @@ -51,6 +52,7 @@ export default function App() {
<Route path='/course/:id+' component={CoursePage} />
<Route path='/professor/:id' component={ProfessorPage} />
<Route path='/admin' component={AdminPage} />
<Route path='/reviews' component={ReviewsPage} />
<Route component={ErrorPage} />
</Switch>
<Footer />
Expand Down
Binary file added site/src/asset/searching.webp
Binary file not shown.
22 changes: 18 additions & 4 deletions site/src/component/GradeDist/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default class Chart extends React.Component<ChartProps> {
* Create an array of objects to feed into the chart.
* @return an array of JSON objects detailing the grades for each class
*/
getClassData = () => {
getClassData = (): Bar[] => {
let gradeACount = 0, gradeBCount = 0, gradeCCount = 0, gradeDCount = 0,
gradeFCount = 0, gradePCount = 0, gradeNPCount = 0;

Expand Down Expand Up @@ -131,16 +131,30 @@ export default class Chart extends React.Component<ChartProps> {
* @return a JSX block rendering the chart
*/
render() {
const data = this.getClassData()

// greatestCount calculates the upper bound of the graph (i.e. the greatest number of students in a single grade)
const greatestCount = data.reduce((max, grade) => (
grade[grade.id] as number > max
? grade[grade.id] as number
: max
), 0);

// The base marginX is 30, with increments of 5 added on for every order of magnitude greater than 100 to accomadate for larger axis labels (1,000, 10,000, etc)
// For example, if greatestCount is 5173 it is (when rounding down (i.e. floor)), one magnitude (calculated with log_10) greater than 100, therefore we add one increment of 5px to our base marginX of 30px
// Math.max() ensures that we're not finding the log of a non-positive number
const marginX = 30 + (5 * Math.floor(Math.log10(Math.max(100, greatestCount) / 100)))

return <>
<ResponsiveBar
data={this.getClassData()}
data={data}
keys={['A', 'B', 'C', 'D', 'F', 'P', 'NP']}
indexBy='label'
margin={{
top: 50,
right: 30,
right: marginX,
bottom: 50,
left: 30
left: marginX,
}}
layout='vertical'
axisBottom={{
Expand Down
19 changes: 11 additions & 8 deletions site/src/component/GradeDist/GradeDist.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
width: 100%;
display: flex;
flex-direction: column;
margin-left: 1vw;

.gradedist-filter {
margin: 1vh;
Expand All @@ -16,11 +15,14 @@

#menu {
display: flex;
margin: 0;
}

#chart {
display: flex;
justify-content: space-evenly;
margin-left: 0;
margin-right: 0;
}

.grade_distribution_chart-container {
Expand All @@ -45,17 +47,20 @@

#menu {
padding-top: 2vh;
justify-content: flex-start;
}

.chart {
width: 80%;
}

.pie {
width: 80%;
}
}

@media only screen and (max-width: 600px) {
#chart {
flex-direction: column;
flex-direction: column;
}

.chart {
Expand Down Expand Up @@ -83,15 +88,13 @@
}
}

@media only screen and (min-device-width : 1320px)
and (max-device-width : 1440px) {
@media only screen and (min-device-width: 1320px) and (max-device-width: 1440px) {
.pie-text {
font-size: 1.2em;
}
}
@media only screen and (min-device-width : 1441px)
and (max-device-width : 1600px) {
@media only screen and (min-device-width: 1441px) and (max-device-width: 1600px) {
.pie-text {
font-size: 1.3em;
}
}
}
4 changes: 2 additions & 2 deletions site/src/component/GradeDist/GradeDist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,8 @@ const GradeDist: FC<GradeDistProps> = (props) => {
</Grid.Row>
</div>
);
} else if (gradeDistData == null) { // null if still fetching, don't display anything while it still loads
return null;
} else if (gradeDistData == null) { // null if still fetching, display loading message
return <>Loading Distribution..</>;
} else { // gradeDistData is empty, did not receive any data from API call or received an error, display an error message
return (
<>
Expand Down
21 changes: 13 additions & 8 deletions site/src/component/GradeDist/Pie.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export default class Pie extends React.Component<PieProps> {

render() {
return (
<div style={{ width: '100%' }}>
<div style={{ width: '100%', position: 'relative' }}>
<ResponsivePie<Slice>
data={this.getClassData()}
margin={{
Expand Down Expand Up @@ -173,13 +173,18 @@ export default class Pie extends React.Component<PieProps> {
</div>
)}
/>
<div style={{ display: 'flex', textAlign: 'center', margin: '-235px' }}>
<div style={{ margin: 'auto' }}>
{this.totalPNP == this.total ? <h3 className='pie-text'>Average Grade: {this.averagePNP}</h3> : null}
{this.totalPNP != this.total ? <h3 className='pie-text'>Average Grade: {this.averageGrade} ({this.averageGPA})</h3> : null}
<h3 className='pie-text' style={{ marginBottom: '6px' }}>Total Enrolled: <strong>{this.total}</strong></h3>
{this.totalPNP > 0 ? <small>{this.totalPNP} enrolled as P/NP</small> : null}
</div>
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
textAlign: 'center',
width: '100%'
}}>
{this.totalPNP == this.total ? <h3 className='pie-text'>Average Grade: {this.averagePNP}</h3> : null}
{this.totalPNP != this.total ? <h3 className='pie-text'>Average Grade: {this.averageGrade} ({this.averageGPA})</h3> : null}
<h3 className='pie-text' style={{ marginBottom: '6px' }}>Total Enrolled: <strong>{this.total}</strong></h3>
{this.totalPNP > 0 ? <small>{this.totalPNP} enrolled as P/NP</small> : null}
</div>
</div>
)
Expand Down
26 changes: 14 additions & 12 deletions site/src/component/PrereqTree/PrereqTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,18 +154,20 @@ const PrereqTree: FC<PrereqProps> = (props) => {
</div>} */}

</div>
<div
style={{
padding: '1em',
backgroundColor: '#f5f5f5',
marginTop: '2em',
}}
>
<p>
{props.prerequisite_text !== '' && <b>Prerequisite: </b>}
{props.prerequisite_text}
</p>
</div>
{props.prerequisite_text !== '' && (
<div
style={{
padding: '1em',
backgroundColor: '#f5f5f5',
marginTop: '2em',
}}
>
<p>
<b>Prerequisite: </b>
{props.prerequisite_text}
</p>
</div>
)}
</Grid.Row>
</div>
);
Expand Down
Loading

0 comments on commit dd744f6

Please sign in to comment.