Skip to content

Commit b2f48c2

Browse files
jklugeSailet03
andauthored
README Documentation & Firebase refactoring (#130)
* Locations and Departments * Not working functions stuff... * Reviews need login * Only one review per course * We update the averages automatically * Bug Fixes Yippeee * Bug Fixes, handle reviews and show errors * More ratings * Refactoring * Readme update * Even more README updates * Made the firebase safer and wrote more documentation in the README. * Update README.md * Update README.md * Update README.md * Update README.md * Reverted the environment refactoring --------- Co-authored-by: Sailet03 <52610280+Sailet03@users.noreply.github.com>
1 parent d17a377 commit b2f48c2

File tree

4 files changed

+245
-48
lines changed

4 files changed

+245
-48
lines changed

README.md

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
# Course-Compass
22
## by team [Inference](https://inferencekth.github.io/Course-Compass/)
3-
Course-Compass is a webpage for interacting with the kth courses via the kth api. It allows for searching and filtering through all active courses.
3+
Course-Compass is an interactive web application for exploring KTH courses. It allows users to search, filter, and review courses while providing prerequisite visualization and personalized recommendations. The application uses Firebase for data storage and real-time updates.
4+
5+
6+
## Features
7+
- Course search with advanced filtering
8+
- Course reviews and ratings
9+
- Interactive prerequisite visualization
10+
- Transcript upload for eligibility checking
11+
- Personal course favorites
12+
- Dark/Light mode support
413

514
## How to run
615

@@ -19,7 +28,6 @@ docker-compose up
1928
```
2029
builds and starts the container.
2130

22-
2331
### Building with NPM
2432
After downloading the repository navigate to the folder my-app and install the dependencies with
2533

@@ -36,9 +44,127 @@ for production use
3644
npm run build
3745
```
3846

47+
## Environment Setup
48+
49+
### Firebase Configuration
50+
This project uses Firebase for backend services. To set up your development environment:
51+
52+
Update the api keys in firebase.js to your keys.
53+
54+
```js
55+
const firebaseConfig = {
56+
apiKey: "",
57+
authDomain: "",
58+
databaseURL:"",
59+
projectId: "",
60+
storageBucket: "",
61+
messagingSenderId: "",
62+
appId: "",
63+
};
64+
```
65+
66+
### Database Population
67+
To populate the Firebase database with course data:
68+
69+
1. Use the JSON file in `/src/assets/example.json` or prepare a file according to the following outline:
70+
```json
71+
{
72+
"courseCode": {
73+
"code": "string",
74+
"name": "string",
75+
"location": "string",
76+
"department": "string",
77+
"language": "string",
78+
"description": "string",
79+
"academic_level": "string",
80+
"periods": "array",
81+
"credits": "number",
82+
"prerequisites": "object",
83+
"prerequisites_text": "string",
84+
"learning_outcomes": "string"
85+
}
86+
}
87+
```
88+
89+
2. Use the `model.populateDatabase(data)` function to upload courses:
90+
```javascript
91+
import data from "./assets/example.json";
92+
model.populateDatabase(data);
93+
```
94+
95+
### Firebase Security Rules
96+
<details>
97+
<summary>Click to view Firebase Rules</summary>
98+
99+
```json
100+
{
101+
"rules": {
102+
"courses": {
103+
".read": true,
104+
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
105+
},
106+
"metadata": {
107+
".read": true,
108+
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
109+
},
110+
"departments": {
111+
".read": true,
112+
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
113+
},
114+
"locations": {
115+
".read": true,
116+
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
117+
},
118+
"reviews": {
119+
".read": true,
120+
"$courseCode": {
121+
"$userID": {
122+
".write": "auth != null && (auth.uid === $userID || data.child('uid').val() === auth.uid || !data.exists())",
123+
".validate": "newData.hasChildren(['text', 'timestamp']) && newData.child('text').isString() && newData.child('timestamp').isNumber()"
124+
}
125+
}
126+
},
127+
"users": {
128+
"$userID": {
129+
".read": "auth != null && auth.uid === $userID",
130+
".write": "auth != null && auth.uid === $userID"
131+
}
132+
}
133+
}
134+
}
135+
```
136+
</details>
137+
138+
To deploy these rules:
139+
```bash
140+
firebase deploy --only database
141+
```
142+
143+
### ⚠️ Security Notes
144+
- Never commit `.env.local` to version control
145+
- Keep your Firebase credentials secure
146+
- Contact the team lead if you need access to the Firebase configuration
147+
39148
## Project structure
40-
The project uses the **[Model–view–presenter (MVP)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter)** paradime. The view displays the data. The presenter contains the logic. The model contains the data.
149+
The project uses the **[Model–view–presenter (MVP)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter)** paradigm. The view displays the data. The presenter contains the logic. The model contains the data.
150+
151+
### Key Components
152+
- **/src/model.js**: Core data model and business logic
153+
- **/src/views/**: UI components and layouts
154+
- **/src/presenters/**: Interface between Model and View
155+
- **/src/scripts/**: Utility scripts including transcript parsing
156+
- **/src/assets/**: Static resources and images
157+
158+
### Development Components
159+
- **/src/dev/**: Development utilities and component previews
160+
- **/src/presenters/Tests/**: Test implementations
161+
- **/scripts/transcript-scraper/**: Transcript parsing tools
162+
163+
164+
### Project Tree
41165

166+
<details>
167+
<summary>Click to view Project Tree</summary>
42168

43169
```
44170
.
@@ -153,10 +279,11 @@ The project uses the **[Model–view–presenter (MVP)](https://en.wikipedia.org
153279
21 directories, 87 files
154280
```
155281

282+
</details>
156283

157284
## Other branches
158285

159-
The **[docs](https://github.com/InferenceKTH/Course-Compass/tree/kth-api)** branch contains the team website.
286+
The **[docs](https://github.com/InferenceKTH/Course-Compass/tree/docs)** branch contains the team website.
160287

161288
The **[kth-api](https://github.com/InferenceKTH/Course-Compass/tree/kth-api)** contains most of the tools used for gathering and processing the course info.
162289

my-app/.gitignore

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,45 @@ yarn-debug.log*
66
yarn-error.log*
77
pnpm-debug.log*
88
lerna-debug.log*
9+
10+
# Dependencies
911
/node_modules
10-
.firebase
12+
/.pnp
13+
.pnp.js
14+
/.firebase
15+
16+
# Testing
17+
/coverage
1118

12-
node_modules
13-
dist
19+
# Production
20+
/build
21+
/dist
1422
dist-ssr
1523
*.local
1624

25+
# Environment Variables
26+
.env
27+
.env.local
28+
.env.development.local
29+
.env.test.local
30+
.env.production.local
31+
1732
# Editor directories and files
1833
.vscode/*
1934
!.vscode/extensions.json
2035
.idea
21-
.DS_Store
2236
*.suo
2337
*.ntvs*
2438
*.njsproj
2539
*.sln
2640
*.sw?
2741

42+
# OS generated files
43+
.DS_Store
44+
.DS_Store?
45+
._*
46+
.Spotlight-V100
47+
.Trashes
48+
ehthumbs.db
49+
Thumbs.db
50+

my-app/firebase.js

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { initializeApp } from "firebase/app";
22
import { getAuth, GoogleAuthProvider, onAuthStateChanged } from "firebase/auth";
33
import { getFunctions, httpsCallable } from 'firebase/functions';
4-
import { get, getDatabase, ref, set, onValue, onChildRemoved, onChildAdded } from "firebase/database";
4+
import { get, getDatabase, ref, set, onValue, onChildRemoved, onChildAdded, runTransaction } from "firebase/database";
55
import { reaction, toJS } from "mobx";
66

77
// Your web app's Firebase configuration
@@ -24,7 +24,6 @@ export const db = getDatabase(app);
2424
export const googleProvider = new GoogleAuthProvider();
2525
googleProvider.addScope("profile");
2626
googleProvider.addScope("email");
27-
let noUpload = false;
2827

2928
export function connectToFirebase(model) {
3029
loadCoursesFromCacheOrFirebase(model);
@@ -61,42 +60,46 @@ export function connectToFirebase(model) {
6160

6261
// fetches all relevant information to create the model
6362
async function firebaseToModel(model) {
64-
const userRef = ref(db, `users/${model.user.uid}`);
65-
onValue(userRef, (snapshot) => {
66-
if (!snapshot.exists()) return;
67-
const data = snapshot.val();
68-
noUpload = true;
69-
if (data?.favourites) model.setFavourite(data.favourites);
70-
if (data?.currentSearchText)
71-
model.setCurrentSearchText(data.currentSearchText);
72-
// if (data.scrollPosition)
73-
// model.setScrollPosition(data.scrollPosition);
74-
// if (data.filterOptions) model.setFilterOptions(data.filterOptions);
75-
noUpload = false;
76-
});
63+
const userRef = ref(db, `users/${model.user.uid}`);
64+
onValue(userRef, async (snapshot) => {
65+
if (!snapshot.exists()) return;
66+
const data = snapshot.val();
67+
68+
// Use a transaction to ensure atomicity
69+
await runTransaction(userRef, (currentData) => {
70+
if (currentData) {
71+
if (data?.favourites) model.setFavourite(data.favourites);
72+
if (data?.currentSearchText) model.setCurrentSearchText(data.currentSearchText);
73+
// Add other fields as needed
74+
}
75+
return currentData; // Return the current data to avoid overwriting
76+
});
77+
});
7778
}
7879

7980
export function syncModelToFirebase(model) {
80-
reaction(
81-
() => ({
82-
userId: model?.user.uid,
83-
favourites: toJS(model.favourites),
84-
currentSearchText: toJS(model.currentSearchText),
85-
// filterOptions: toJS(model.filterOptions),
86-
// Add more per-user attributes here
87-
}),
88-
// eslint-disable-next-line no-unused-vars
89-
({ userId, favourites, currentSearchText }) => {
90-
if (noUpload || !userId) return;
91-
const userRef = ref(db, "users/${userId}");
92-
const dataToSync = {
93-
favourites,
94-
currentSearchText,
95-
// filterOptions,
96-
};
97-
set(userRef, dataToSync).catch(console.error);
98-
}
99-
);
81+
reaction(
82+
() => ({
83+
userId: model?.user.uid,
84+
favourites: toJS(model.favourites),
85+
currentSearchText: toJS(model.currentSearchText),
86+
}),
87+
async ({ userId, favourites, currentSearchText }) => {
88+
if (!userId) return;
89+
90+
const userRef = ref(db, `users/${userId}`);
91+
await runTransaction(userRef, (currentData) => {
92+
// Merge the new data with the existing data
93+
return {
94+
...currentData,
95+
favourites,
96+
currentSearchText,
97+
};
98+
}).catch((error) => {
99+
console.error('Error syncing model to Firebase:', error);
100+
});
101+
}
102+
);
100103
}
101104

102105
export function syncScrollPositionToFirebase(model, containerRef) {
@@ -172,9 +175,12 @@ async function fetchLastUpdatedTimestamp() {
172175
}
173176

174177
export async function addCourse(course) {
175-
if (!course?.code) return;
176-
const myRef = ref(db, `courses/${course.code}`);
177-
await set(myRef, course);
178+
if (!auth.currentUser)
179+
throw new Error('User must be authenticated');
180+
if (!course?.code)
181+
throw new Error('Invalid course data');
182+
const myRef = ref(db, `courses/${course.code}`);
183+
await set(myRef, course);
178184
updateLastUpdatedTimestamp();
179185
}
180186

@@ -322,9 +328,8 @@ function startAverageRatingListener(model) {
322328
initialRatings[courseCode] = avgRating;
323329
}
324330
});
325-
326331
model.setAverageRatings(initialRatings);
327-
});
332+
})
328333
}
329334

330335
// Step 2: listener for each courses avgRating

my-app/firebase_rules.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"rules": {
3+
// Courses and Metadata
4+
"courses": {
5+
".read": true,
6+
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
7+
},
8+
"metadata": {
9+
".read": true,
10+
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
11+
},
12+
"departments": {
13+
".read": true,
14+
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
15+
},
16+
"locations": {
17+
".read": true,
18+
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
19+
},
20+
21+
// Reviews
22+
"reviews": {
23+
".read":true,
24+
"$courseCode": {
25+
"$userID": {
26+
".write": "auth != null && (auth.uid === $userID || data.child('uid').val() === auth.uid || !data.exists())",
27+
".validate": "newData.hasChildren(['text', 'timestamp']) &&
28+
newData.child('text').isString() &&
29+
newData.child('timestamp').isNumber()"
30+
}
31+
}
32+
},
33+
34+
// Users
35+
"users": {
36+
"$userID": {
37+
".read": "auth != null && auth.uid === $userID",
38+
".write": "auth != null && auth.uid === $userID"
39+
}
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)