Skip to content

Commit c64c6ee

Browse files
authored
added avg rating. fixed some bugs (#77)
1 parent 4e1bbd7 commit c64c6ee

File tree

12 files changed

+240
-78
lines changed

12 files changed

+240
-78
lines changed

my-app/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"react-infinite-scroll-component": "^6.1.0",
2929
"react-router-dom": "^7.4.0",
3030
"reactflow": "^11.11.4",
31-
"tailwindcss": "^4.0.17"
31+
"tailwindcss": "^4.0.17",
32+
"@react-buddy/ide-toolbox": "^2.4.0"
3233
},
3334
"devDependencies": {
3435
"@eslint/js": "^9.21.0",

my-app/src/dev/README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
This directory contains utility files which enable some visual features of the
2+
[React Buddy](https://plugins.jetbrains.com/plugin/17467-react-buddy/) plugin.
3+
Files in the directory should be committed to source control.
4+
5+
React Buddy palettes describe reusable components and building blocks. `React Palette` tool window becomes available
6+
when an editor with React components is active. You can drag and drop items from the tool window to the code editor or
7+
JSX Outline. Alternatively, you can insert components from the palette using code generation action (`alt+insert` /
8+
`⌘ N`).
9+
10+
Add components to the palette using `Add to React Palette` intention or via palette editor (look for the corresponding
11+
link in `palette.tsx`). There are some ready-to-use palettes for popular React libraries which are published as npm
12+
packages and can be added as a dependency:
13+
14+
```jsx
15+
import AntdPalette from "@react-buddy/palette-antd";
16+
import ReactIntlPalette from "@react-buddy/palette-react-intl";
17+
18+
export const PaletteTree = () => (
19+
<Palette>
20+
<AntdPalette/>
21+
<ReactIntlPalette/>
22+
<Category name="App templates">
23+
<Component name="Card">
24+
<Variant name="Loading">
25+
<Card title="Card title">
26+
<Skeleton loading={true} avatar active>
27+
Card content
28+
</Skeleton>
29+
</Card>
30+
</Variant>
31+
</Component>
32+
<Component name="Form">
33+
<Variant proto={FormTemplate}/>
34+
</Component>
35+
</Category>
36+
</Palette>
37+
)
38+
```
39+
40+
React Buddy explicitly registers any previewed component in the `previews.tsx` file so that you can specify required
41+
props.
42+
43+
```jsx
44+
<ComponentPreview path="/Page">
45+
<Page title={'Hello'}/>
46+
</ComponentPreview>
47+
```
48+
49+
You can add some global initialization logic for the preview mode in `useInitital.ts`,
50+
e.g. implicitly obtain user session:
51+
52+
```typescript
53+
export const useInitial: () => InitialHookStatus = () => {
54+
const [loading, setLoading] = useState<boolean>(false);
55+
const [error, setError] = useState<boolean>(false);
56+
57+
useEffect(() => {
58+
setLoading(true);
59+
async function login() {
60+
const response = await loginRequest(DEV_LOGIN, DEV_PASSWORD);
61+
if (response?.status !== 200) {
62+
setError(true);
63+
}
64+
setLoading(false);
65+
}
66+
login();
67+
}, []);
68+
return { loading, error };
69+
};
70+
```

my-app/src/dev/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from "react"
2+
import {useInitial} from "./useInitial"
3+
4+
const ComponentPreviews = React.lazy(() => import("./previews"))
5+
6+
export {
7+
ComponentPreviews,
8+
useInitial
9+
}

my-app/src/dev/palette.jsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {Fragment} from "react"
2+
import {
3+
Category,
4+
Component,
5+
Variant,
6+
Palette,
7+
} from "@react-buddy/ide-toolbox"
8+
9+
export const PaletteTree = () => (
10+
<Palette>
11+
<Category name="App">
12+
<Component name="Loader">
13+
<Variant>
14+
<ExampleLoaderComponent/>
15+
</Variant>
16+
</Component>
17+
</Category>
18+
</Palette>
19+
)
20+
21+
export function ExampleLoaderComponent() {
22+
return (
23+
<Fragment>Loading...</Fragment>
24+
)
25+
}

my-app/src/dev/previews.jsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {Previews} from '@react-buddy/ide-toolbox'
2+
import {PaletteTree} from './palette'
3+
4+
const ComponentPreviews = () => {
5+
return (
6+
<Previews palette={<PaletteTree/>}>
7+
</Previews>
8+
)
9+
}
10+
11+
export default ComponentPreviews

my-app/src/dev/useInitial.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {useState} from 'react'
2+
3+
export const useInitial = () => {
4+
const [status, setStatus] = useState({
5+
loading: false,
6+
error: false
7+
})
8+
/*
9+
Implement hook functionality here.
10+
If you need to execute async operation, set loading to true and when it's over, set loading to false.
11+
If you caught some errors, set error status to true.
12+
Initial hook is considered to be successfully completed if it will return {loading: false, error: false}.
13+
*/
14+
return status
15+
}

my-app/src/model.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,12 @@ export const model = {
164164
// this.filterOptions.applyDepartmentFilter = departmentFilterState;
165165
// },
166166

167+
async getAverageRating(courseCode) {
168+
const reviews = await getReviewsForCourse(courseCode);
169+
if (!reviews || reviews.length === 0) return null;
170+
const total = reviews.reduce((sum, review) => sum + (review.overallRating || 0), 0);
171+
return (total / reviews.length).toFixed(1);
172+
},
167173

168174

169175
};

my-app/src/presenters/ListViewPresenter.jsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ const ListViewPresenter = observer(({ model }) => {
1616
const container = scrollContainerRef.current;
1717
if (!container || !model.scrollPosition) return;
1818

19-
19+
2020

2121
const attemptScroll = () => {
2222

2323
// refresh on significant change (same as in firebase)
24-
if (Math.abs(container.scrollTop - model.scrollPosition) < 100)
24+
if (Math.abs(container.scrollTop - model.scrollPosition) < 100)
2525
return;
2626

2727
attempts++;
@@ -43,7 +43,7 @@ const ListViewPresenter = observer(({ model }) => {
4343

4444
useEffect(() => {
4545
// Load initial scroll position
46-
const savedPosition = model.user
46+
const savedPosition = model.user
4747
? model.scrollPosition
4848
: localStorage.getItem("scrollPosition");
4949
if (savedPosition) {
@@ -88,15 +88,16 @@ const ListViewPresenter = observer(({ model }) => {
8888
isOpen={isPopupOpen} onClose={() => setIsPopupOpen(false)}
8989
course={selectedCourse}
9090
prerequisiteTree={preP}
91-
reviewPresenter={reviewPresenter} />
91+
reviewPresenter={reviewPresenter}/>
92+
9293

9394

9495

9596
return <ListView
9697
courses={model.courses}
9798
searchResults={model.currentSearch}
9899
currentSearchLenght={model.currentSearch.length}
99-
100+
100101
favouriteCourses={model.favourites}
101102
addFavourite={addFavourite}
102103
removeFavourite={removeFavourite}
@@ -106,7 +107,7 @@ const ListViewPresenter = observer(({ model }) => {
106107
setIsPopupOpen={setIsPopupOpen}
107108
setSelectedCourse={setSelectedCourse}
108109
popup={popup}
109-
110+
110111
targetScroll={model.scrollPosition}
111112
scrollContainerRef={scrollContainerRef}
112113
persistantScrolling={persistantScrolling}

my-app/src/views/Components/CoursePagePopup.jsx

Lines changed: 90 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,36 @@
11
import React, { useEffect, useRef, useState } from 'react';
2+
import RatingComponent from "./RatingComponent.jsx";
3+
import { model } from "../../model.js";
24

35
function CoursePagePopup({
4-
favouriteCourses,
5-
handleFavouriteClick,
6-
isOpen,
7-
onClose,
8-
course,
9-
prerequisiteTree,
10-
reviewPresenter,
11-
}) {
6+
favouriteCourses,
7+
handleFavouriteClick,
8+
isOpen,
9+
onClose,
10+
course,
11+
prerequisiteTree,
12+
reviewPresenter,
13+
}) {
14+
1215
const treeRef = useRef(null);
1316
const [showOverlay, setShowOverlay] = useState(true);
17+
const [averageRating, setAverageRating] = useState(null);
18+
19+
20+
useEffect(() => {
21+
const fetchAverageRating = async () => {
22+
try {
23+
const avg = await model.getAverageRating(course.code);
24+
setAverageRating(avg);
25+
} catch (error) {
26+
setAverageRating(null);
27+
}
28+
};
29+
30+
if (isOpen && course) fetchAverageRating();
31+
32+
}, [isOpen, course]);
33+
1434

1535
useEffect(() => {
1636
const handleKeyDown = (event) => {
@@ -53,42 +73,74 @@ function CoursePagePopup({
5373
<h2 className="text-5xl font-extrabold text-[#2e2e4f]">
5474
<span className="text-violet-700">{course.code}</span> - {course.name}
5575
<span className="ml-4 text-lg text-violet-700 whitespace-nowrap">
56-
({course.credits} Credits)
57-
</span>
76+
({course.credits} Credits)
77+
</span>
5878
</h2>
5979
<div className="my-6 h-1.5 w-full bg-violet-500"></div>
6080
</div>
61-
<div>
81+
<div className="flex justify-between items-center">
6282
<button
6383
className={`inline-flex items-center px-4 py-2 gap-2 rounded-lg
64-
transition-all duration-300 ease-in-out
65-
font-semibold text-sm shadow-sm
66-
${favouriteCourses.some((fav) => fav.code === course.code)
67-
? 'bg-yellow-400 /90 hover:bg-yellow-500/90 border-2 border-yellow-600 hover:border-yellow-700 text-yellow-900'
68-
: 'bg-yellow-200/90 hover:bg-yellow-300 border-2 border-yellow-400 hover:border-yellow-500 text-yellow-600 hover:text-yellow-700'
69-
}`}
84+
transition-all duration-300 ease-in-out
85+
font-semibold text-sm shadow-sm
86+
${
87+
favouriteCourses.some((fav) => fav.code === course.code)
88+
? 'bg-yellow-400/90 hover:bg-yellow-500/90 border-2 border-yellow-600 hover:border-yellow-700 text-yellow-900'
89+
: 'bg-yellow-200/90 hover:bg-yellow-300 border-2 border-yellow-400 hover:border-yellow-500 text-yellow-600 hover:text-yellow-700'
90+
}`}
7091
onClick={(e) => {
7192
e.stopPropagation();
7293
handleFavouriteClick(course);
7394
}}
7495
>
7596
{favouriteCourses.some((fav) => fav.code === course.code) ? (
7697
<>
77-
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 fill-yellow-900" viewBox="0 0 20 20">
78-
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
98+
<svg
99+
xmlns="http://www.w3.org/2000/svg"
100+
className="h-5 w-5 fill-yellow-900"
101+
viewBox="0 0 20 20"
102+
>
103+
<path
104+
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"
105+
/>
79106
</svg>
80107
Remove from Favourites
81108
</>
82109
) : (
83110
<>
84-
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 stroke-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
85-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
111+
<svg
112+
xmlns="http://www.w3.org/2000/svg"
113+
className="h-5 w-5 stroke-yellow-500"
114+
fill="none"
115+
viewBox="0 0 24 24"
116+
stroke="currentColor"
117+
>
118+
<path
119+
strokeLinecap="round"
120+
strokeLinejoin="round"
121+
strokeWidth={2}
122+
d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"
123+
/>
86124
</svg>
87125
Add to Favourites
88126
</>
89127
)}
90128
</button>
129+
130+
<div className="flex flex-col items-center">
131+
{averageRating !== null ? (
132+
<p className="text-lg font-semibold text-violet-700">
133+
Average Rating: {averageRating} / 5
134+
</p>
135+
) : (
136+
<p className="text-lg font-semibold text-violet-700">
137+
No Reviews Yet
138+
</p>
139+
)}
140+
<RatingComponent readOnly={true} value={averageRating || 0} />
141+
</div>
91142
</div>
143+
92144
{/* Description Section */}
93145
<div>
94146
<h3 className="text-2xl font-bold text-[#2e2e4f] mb-0.5">Course Description</h3>
@@ -102,26 +154,25 @@ function CoursePagePopup({
102154
<div>
103155
<h3 className="text-2xl font-semibold text-[#2e2e4f] mb-0.5">Prerequisite Graph Tree</h3>
104156
<div className="mb-4 h-0.5 w-full bg-violet-500 rounded-lg"></div>
105-
<div className="relative rounded-lg">
106-
{showOverlay && (
107-
<div
108-
className="absolute inset-0 z-10 bg-indigo-200/10 rounded-lg cursor-pointer flex items-center justify-center z-51"
109-
onClick={(e) => {
110-
e.stopPropagation();
111-
setShowOverlay(false);
112-
}}
113-
>
114-
</div>
115-
)}
157+
<div className="relative rounded-lg">
158+
{showOverlay && (
116159
<div
117-
className="bg-indigo-300/50 outline-none focus:outline-none focus:ring-2 focus:ring-violet-600 rounded-lg transition-shadow"
118-
ref={treeRef}
119-
onClick={handleTreeClick}
120-
tabIndex={0}
121-
>
122-
{prerequisiteTree}
123-
</div>
160+
className="absolute inset-0 z-10 bg-indigo-200/10 rounded-lg cursor-pointer flex items-center justify-center z-51"
161+
onClick={(e) => {
162+
e.stopPropagation();
163+
setShowOverlay(false);
164+
}}
165+
></div>
166+
)}
167+
<div
168+
className="bg-indigo-300/50 outline-none focus:outline-none focus:ring-2 focus:ring-violet-600 rounded-lg transition-shadow"
169+
ref={treeRef}
170+
onClick={handleTreeClick}
171+
tabIndex={0}
172+
>
173+
{prerequisiteTree}
124174
</div>
175+
</div>
125176
</div>
126177
{/* Reviews Section (optional) */}
127178
{reviewPresenter && (

0 commit comments

Comments
 (0)