Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
596066a
Locations and Departments
jkluge May 8, 2025
6a72592
Merge branch 'main' of github.com:jkluge/Find-My-Next-Course into db
jkluge May 8, 2025
3e701d1
Not working functions stuff...
jkluge May 8, 2025
ed5724b
Reviews need login
jkluge May 8, 2025
e334265
Only one review per course
jkluge May 8, 2025
43fb6c3
Merge branch 'reviews-db' of github.com:jkluge/Find-My-Next-Course in…
jkluge May 8, 2025
3911646
Merge branch 'db' of github.com:jkluge/Find-My-Next-Course into revie…
jkluge May 9, 2025
0ab8bcd
We update the averages automatically
jkluge May 9, 2025
6cede52
Bug Fixes Yippeee
jkluge May 9, 2025
6a5a083
Bug Fixes, handle reviews and show errors
jkluge May 9, 2025
16eb71a
Merge branch 'main' of github.com:jkluge/Find-My-Next-Course into rev…
jkluge May 9, 2025
cea14e2
Merge branch 'main' of github.com:jkluge/Find-My-Next-Course into rev…
jkluge May 9, 2025
aee13d0
More ratings
jkluge May 9, 2025
74d1f4d
Merge branch 'main' of github.com:jkluge/Find-My-Next-Course into rev…
jkluge May 9, 2025
f223ece
Refactoring
jkluge May 9, 2025
91e8479
Readme update
jkluge May 9, 2025
f7b29be
Even more README updates
jkluge May 9, 2025
2801fef
Merge branch 'main' of github.com:jkluge/Find-My-Next-Course into rev…
jkluge May 9, 2025
ba56ef8
Made the firebase safer and wrote more documentation in the README.
jkluge May 9, 2025
c3b8a20
Update README.md
Sailet03 May 9, 2025
6d06076
Update README.md
Sailet03 May 9, 2025
683d5ca
Update README.md
Sailet03 May 9, 2025
5384bef
Update README.md
Sailet03 May 9, 2025
6f41a7c
Reverted the environment refactoring
jkluge May 9, 2025
f30c482
Merge branch 'reviews-db' of github.com:jkluge/Find-My-Next-Course in…
jkluge May 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 131 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# Course-Compass
## by team [Inference](https://inferencekth.github.io/Course-Compass/)
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.
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.


## Features
- Course search with advanced filtering
- Course reviews and ratings
- Interactive prerequisite visualization
- Transcript upload for eligibility checking
- Personal course favorites
- Dark/Light mode support

## How to run

Expand All @@ -19,7 +28,6 @@ docker-compose up
```
builds and starts the container.


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

Expand All @@ -36,9 +44,127 @@ for production use
npm run build
```

## Environment Setup

### Firebase Configuration
This project uses Firebase for backend services. To set up your development environment:

Update the api keys in firebase.js to your keys.

```js
const firebaseConfig = {
apiKey: "",
authDomain: "",
databaseURL:"",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: "",
};
```

### Database Population
To populate the Firebase database with course data:

1. Use the JSON file in `/src/assets/example.json` or prepare a file according to the following outline:
```json
{
"courseCode": {
"code": "string",
"name": "string",
"location": "string",
"department": "string",
"language": "string",
"description": "string",
"academic_level": "string",
"periods": "array",
"credits": "number",
"prerequisites": "object",
"prerequisites_text": "string",
"learning_outcomes": "string"
}
}
```

2. Use the `model.populateDatabase(data)` function to upload courses:
```javascript
import data from "./assets/example.json";
model.populateDatabase(data);
```

### Firebase Security Rules
<details>
<summary>Click to view Firebase Rules</summary>

```json
{
"rules": {
"courses": {
".read": true,
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
},
"metadata": {
".read": true,
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
},
"departments": {
".read": true,
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
},
"locations": {
".read": true,
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
},
"reviews": {
".read": true,
"$courseCode": {
"$userID": {
".write": "auth != null && (auth.uid === $userID || data.child('uid').val() === auth.uid || !data.exists())",
".validate": "newData.hasChildren(['text', 'timestamp']) && newData.child('text').isString() && newData.child('timestamp').isNumber()"
}
}
},
"users": {
"$userID": {
".read": "auth != null && auth.uid === $userID",
".write": "auth != null && auth.uid === $userID"
}
}
}
}
```
</details>

To deploy these rules:
```bash
firebase deploy --only database
```

### ⚠️ Security Notes
- Never commit `.env.local` to version control
- Keep your Firebase credentials secure
- Contact the team lead if you need access to the Firebase configuration

## Project structure
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.
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.

### Key Components
- **/src/model.js**: Core data model and business logic
- **/src/views/**: UI components and layouts
- **/src/presenters/**: Interface between Model and View
- **/src/scripts/**: Utility scripts including transcript parsing
- **/src/assets/**: Static resources and images

### Development Components
- **/src/dev/**: Development utilities and component previews
- **/src/presenters/Tests/**: Test implementations
- **/scripts/transcript-scraper/**: Transcript parsing tools


### Project Tree

<details>
<summary>Click to view Project Tree</summary>

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

</details>

## Other branches

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

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.

Expand Down
31 changes: 27 additions & 4 deletions my-app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,45 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

# Dependencies
/node_modules
.firebase
/.pnp
.pnp.js
/.firebase

# Testing
/coverage

node_modules
dist
# Production
/build
/dist
dist-ssr
*.local

# Environment Variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

85 changes: 45 additions & 40 deletions my-app/firebase.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { initializeApp } from "firebase/app";
import { getAuth, GoogleAuthProvider, onAuthStateChanged } from "firebase/auth";
import { getFunctions, httpsCallable } from 'firebase/functions';
import { get, getDatabase, ref, set, onValue, onChildRemoved, onChildAdded } from "firebase/database";
import { get, getDatabase, ref, set, onValue, onChildRemoved, onChildAdded, runTransaction } from "firebase/database";
import { reaction, toJS } from "mobx";

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

export function connectToFirebase(model) {
loadCoursesFromCacheOrFirebase(model);
Expand Down Expand Up @@ -61,42 +60,46 @@ export function connectToFirebase(model) {

// fetches all relevant information to create the model
async function firebaseToModel(model) {
const userRef = ref(db, `users/${model.user.uid}`);
onValue(userRef, (snapshot) => {
if (!snapshot.exists()) return;
const data = snapshot.val();
noUpload = true;
if (data?.favourites) model.setFavourite(data.favourites);
if (data?.currentSearchText)
model.setCurrentSearchText(data.currentSearchText);
// if (data.scrollPosition)
// model.setScrollPosition(data.scrollPosition);
// if (data.filterOptions) model.setFilterOptions(data.filterOptions);
noUpload = false;
});
const userRef = ref(db, `users/${model.user.uid}`);
onValue(userRef, async (snapshot) => {
if (!snapshot.exists()) return;
const data = snapshot.val();

// Use a transaction to ensure atomicity
await runTransaction(userRef, (currentData) => {
if (currentData) {
if (data?.favourites) model.setFavourite(data.favourites);
if (data?.currentSearchText) model.setCurrentSearchText(data.currentSearchText);
// Add other fields as needed
}
return currentData; // Return the current data to avoid overwriting
});
});
}

export function syncModelToFirebase(model) {
reaction(
() => ({
userId: model?.user.uid,
favourites: toJS(model.favourites),
currentSearchText: toJS(model.currentSearchText),
// filterOptions: toJS(model.filterOptions),
// Add more per-user attributes here
}),
// eslint-disable-next-line no-unused-vars
({ userId, favourites, currentSearchText }) => {
if (noUpload || !userId) return;
const userRef = ref(db, "users/${userId}");
const dataToSync = {
favourites,
currentSearchText,
// filterOptions,
};
set(userRef, dataToSync).catch(console.error);
}
);
reaction(
() => ({
userId: model?.user.uid,
favourites: toJS(model.favourites),
currentSearchText: toJS(model.currentSearchText),
}),
async ({ userId, favourites, currentSearchText }) => {
if (!userId) return;

const userRef = ref(db, `users/${userId}`);
await runTransaction(userRef, (currentData) => {
// Merge the new data with the existing data
return {
...currentData,
favourites,
currentSearchText,
};
}).catch((error) => {
console.error('Error syncing model to Firebase:', error);
});
}
);
}

export function syncScrollPositionToFirebase(model, containerRef) {
Expand Down Expand Up @@ -172,9 +175,12 @@ async function fetchLastUpdatedTimestamp() {
}

export async function addCourse(course) {
if (!course?.code) return;
const myRef = ref(db, `courses/${course.code}`);
await set(myRef, course);
if (!auth.currentUser)
throw new Error('User must be authenticated');
if (!course?.code)
throw new Error('Invalid course data');
const myRef = ref(db, `courses/${course.code}`);
await set(myRef, course);
updateLastUpdatedTimestamp();
}

Expand Down Expand Up @@ -322,9 +328,8 @@ function startAverageRatingListener(model) {
initialRatings[courseCode] = avgRating;
}
});

model.setAverageRatings(initialRatings);
});
})
}

// Step 2: listener for each courses avgRating
Expand Down
42 changes: 42 additions & 0 deletions my-app/firebase_rules.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"rules": {
// Courses and Metadata
"courses": {
".read": true,
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
},
"metadata": {
".read": true,
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
},
"departments": {
".read": true,
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
},
"locations": {
".read": true,
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
},

// Reviews
"reviews": {
".read":true,
"$courseCode": {
"$userID": {
".write": "auth != null && (auth.uid === $userID || data.child('uid').val() === auth.uid || !data.exists())",
".validate": "newData.hasChildren(['text', 'timestamp']) &&
newData.child('text').isString() &&
newData.child('timestamp').isNumber()"
}
}
},

// Users
"users": {
"$userID": {
".read": "auth != null && auth.uid === $userID",
".write": "auth != null && auth.uid === $userID"
}
}
}
}