Skip to content

Commit a8ef8ec

Browse files
committed
Merge branch 'main' of github.com:InferenceKTH/Find-My-Next-Course into prereq-oaktree
2 parents 47e67a8 + 6461d03 commit a8ef8ec

36 files changed

Lines changed: 1924 additions & 602 deletions

.github/workflows/docker-build.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Docker CI Build
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
pull_request:
7+
branches: ["main"]
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v4
16+
17+
- name: Set up Docker Buildx
18+
uses: docker/setup-buildx-action@v2
19+
20+
- name: Build and start services with Docker Compose (CI Mode)
21+
run: |
22+
CI=true COMMAND="npx serve -s build" docker compose up -d --build
23+
24+
- name: Check running containers
25+
run: docker ps -a
26+
27+
- name: Check for container failures
28+
run: |
29+
docker compose logs --tail=50
30+
if [ "$(docker compose ps --format '{{.State}}' | grep -c exited)" -gt 0 ]; then
31+
echo "A container has exited with an error. Failing the workflow."
32+
exit 1
33+
fi

.github/workflows/docker-image.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Docker CI
1+
name: Docker CI LINT
22

33
on:
44
push:
@@ -19,7 +19,7 @@ jobs:
1919

2020
- name: Build and start services with Docker Compose (CI Mode)
2121
run: |
22-
CI=true COMMAND="npx serve -s build" docker compose up -d --build
22+
CI=true LINT=true COMMAND="npx serve -s build" docker compose up -d --build
2323
2424
- name: Check running containers
2525
run: docker ps -a

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ WORKDIR /app
33
COPY my-app .
44
RUN npm ci
55
ARG CI=false
6+
ARG LINT=false
67
RUN if [ "$CI" = "true" ]; then npm run build; fi
7-
RUN if [ "$CI" = "true" ]; then npm run lint; fi
8+
RUN if [ "$LINT" = "true" ]; then npm run lint; fi
89
EXPOSE 5173
910
CMD ["npm", "run", "dev"]

my-app/firebase.js

Lines changed: 66 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { initializeApp } from "firebase/app";
22
import { getAuth, GoogleAuthProvider, onAuthStateChanged } from "firebase/auth";
33
import { get, getDatabase, ref, set, onValue, push } from "firebase/database";
44
import { reaction, toJS } from "mobx";
5+
import throttle from "lodash.throttle";
6+
57
// Your web app's Firebase configuration
68
const firebaseConfig = {
79
apiKey: "AIzaSyCBckVI9nhAP62u5jZJW3F4SLulUv7znis",
@@ -24,53 +26,54 @@ googleProvider.addScope("email");
2426
let noUpload = false;
2527

2628
export function connectToFirebase(model) {
27-
loadCoursesFromCacheOrFirebase(model);
29+
loadCoursesFromCacheOrFirebase(model);
2830
onAuthStateChanged(auth, (user) => {
2931
if (user) {
3032
model.setUser(user); // Set the user ID once authenticated
31-
firebaseToModel(model); // Set up listeners for user-specific data
32-
syncModelToFirebase(model); // Start syncing changes to Firebase
33+
firebaseToModel(model); // Set up listeners for user-specific data
34+
syncModelToFirebase(model); // Start syncing changes to Firebase
35+
syncScrollPositionToFirebase(model);
3336
} else {
34-
model.setUser(null); // If no user, clear user-specific data
37+
model.setUser(null); // If no user, clear user-specific data
3538
}
3639
});
3740
}
3841

3942
// fetches all relevant information to create the model
4043
async function firebaseToModel(model) {
41-
if (!model.user)
42-
return;
44+
if (!model.user) return;
4345
const userRef = ref(db, `users/${model.user.uid}`);
4446
onValue(userRef, (snapshot) => {
45-
if (!snapshot.exists())
46-
return;
47+
if (!snapshot.exists()) return;
4748
const data = snapshot.val();
4849
noUpload = true;
49-
if (data.favourites)
50-
model.setFavourite(data.favourites);
51-
// if (data.currentSearch)
52-
// model.setCurrentSearch(data.currentSearch);
50+
if (data.favourites) model.setFavourite(data.favourites);
51+
if (data.currentSearchText)
52+
model.setCurrentSearchText(data.currentSearchText);
53+
if (data.scrollPosition)
54+
model.setScrollPosition(data.scrollPosition);
55+
// if (data.currentSearch)
56+
// model.setCurrentSearch(data.currentSearch);
5357
noUpload = false;
5458
});
5559
}
5660

57-
5861
export function syncModelToFirebase(model) {
5962
reaction(
6063
() => ({
6164
userId: model?.user.uid,
6265
favourites: toJS(model.favourites),
66+
currentSearchText: toJS(model.currentSearchText),
6367
// currentSearch: toJS(model.currentSearch),
6468
// Add more per-user attributes here
6569
}),
6670
// eslint-disable-next-line no-unused-vars
67-
({ userId, favourites, currentSearch }) => {
68-
if (noUpload || !userId)
69-
return;
71+
({ userId, favourites, currentSearchText }) => {
72+
if (noUpload || !userId) return;
7073
const userRef = ref(db, `users/${userId}`);
7174
const dataToSync = {
7275
favourites,
73-
//currentSearch,
76+
currentSearchText,
7477
};
7578

7679
set(userRef, dataToSync)
@@ -80,6 +83,34 @@ export function syncModelToFirebase(model) {
8083
);
8184
}
8285

86+
export function syncScrollPositionToFirebase(model, containerRef) {
87+
if (!containerRef?.current) return;
88+
let lastSavedPosition = 0;
89+
90+
// const throttledSet = throttle((scrollPixel) => {
91+
// if (model?.user?.uid) {
92+
// const userRef = ref(db, `users/${model.user.uid}/scrollPosition`);
93+
// set(userRef, scrollPixel).catch(console.error);
94+
// }
95+
// }, 500);
96+
97+
const handleScroll = () => {
98+
const scrollTop = containerRef.current.scrollTop;
99+
// make a 100px threshold
100+
if (Math.abs(scrollTop - lastSavedPosition) < 100)
101+
return;
102+
103+
lastSavedPosition = scrollTop;
104+
model.setScrollPosition(scrollTop);
105+
localStorage.setItem("scrollPosition", scrollTop);
106+
// throttledSet(scrollTop);
107+
};
108+
109+
containerRef.current.addEventListener('scroll', handleScroll);
110+
return () => containerRef.current?.removeEventListener('scroll', handleScroll);
111+
}
112+
113+
83114
function saveCoursesInChunks(courses, timestamp) {
84115
const parts = 3; // Adjust this based on course size
85116
const chunkSize = Math.ceil(courses.length / parts);
@@ -170,34 +201,26 @@ export async function saveJSONCoursesToFirebase(model, data) {
170201
});
171202
}
172203

173-
174204
export async function addReviewForCourse(courseCode, review) {
175-
try {
176-
const reviewsRef = ref(db, `reviews/${courseCode}`);
177-
const newReviewRef = push(reviewsRef);
178-
await set(newReviewRef, review);
179-
} catch (error) {
180-
console.error("Error when adding a course to firebase:", error);
205+
try {
206+
const reviewsRef = ref(db, `reviews/${courseCode}`);
207+
const newReviewRef = push(reviewsRef);
208+
await set(newReviewRef, review);
209+
} catch (error) {
210+
console.error("Error when adding a course to firebase:", error);
181211
}
182212
}
183213

184-
185214
export async function getReviewsForCourse(courseCode) {
186-
const reviewsRef = ref(db, `reviews/${courseCode}`);
187-
const snapshot = await get(reviewsRef);
188-
if (!snapshot.exists()) return [];
189-
190-
const reviews = [];
191-
snapshot.forEach(childSnapshot => {
192-
reviews.push({
193-
id: childSnapshot.key, // Firebase-generated unique key
194-
userName: childSnapshot.val().userName,
195-
text: childSnapshot.val().text
196-
});
197-
});
198-
return reviews;
199-
}
200-
201-
202-
203-
215+
const reviewsRef = ref(db, `reviews/${courseCode}`);
216+
const snapshot = await get(reviewsRef);
217+
if (!snapshot.exists()) return [];
218+
const reviews = [];
219+
snapshot.forEach(childSnapshot => {
220+
reviews.push({
221+
id: childSnapshot.key,
222+
...childSnapshot.val()
223+
});
224+
});
225+
return reviews;
226+
}

my-app/package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

my-app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"autoprefixer": "^10.4.21",
2020
"firebase": "^11.5.0",
2121
"ldrs": "^1.1.6",
22+
"lodash.throttle": "^4.1.1",
2223
"mobx": "^6.13.7",
2324
"mobx-react-lite": "^4.1.0",
2425
"pdfjs-dist": "^5.1.91",

my-app/src/assets/example.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
"P4": "null"
2020
},
2121
"kth_page_url": "https://www.kth.se/student/kurser/kurs/FEL3310",
22-
"description": "null",
23-
"prerequisites": "null",
24-
"prerequisites_text": "null",
25-
"learning_outcomes": "null"
22+
"description": "TEST",
23+
"prerequisites": "TEST",
24+
"prerequisites_text": "TEST",
25+
"learning_outcomes": "TEST"
2626
},
2727
"FMJ3410": {
2828
"name": "Theory and Methodology of Science for Energy Research",

my-app/src/index.jsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ configure({ enforceActions: "never" });
1515
const reactiveModel = makeAutoObservable(model);
1616
connectToFirebase(reactiveModel);
1717

18-
// ✅ Add /share route here
1918
export function makeRouter(reactiveModel) {
2019
return createHashRouter([
2120
{

my-app/src/model.js

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,32 @@ import { addCourse, addReviewForCourse, getReviewsForCourse } from "../firebase"
33

44
export const model = {
55
user: undefined,
6+
//add searchChange: false, //this is for reworking the searchbar presenter, so that it triggers as a model,
7+
//instead of passing searchcouses lambda function down into the searchbarview.
68
currentSearch: [],
9+
currentSearchText: "",
10+
scrollPosition: 0,
711
courses: [],
812
favourites: [],
13+
isReady: false,
14+
filtersChange: false,
15+
filtersCalculated: false,
16+
filteredCourses: [],
17+
filterOptions: {
18+
applyTranscriptFilter: true,
19+
eligibility: "weak", //the possible values for the string are: "weak"/"moderate"/"strong"
20+
applyLevelFilter: true,
21+
level: [], //the possible values for the array are: "PREPARATORY", "BASIC", "ADVANCED", "RESEARCH"
22+
applyLanguageFilter: true,
23+
language: "none", //the possible values for the string are: "none"/"english"/"swedish"/"both"
24+
applyLocationFilter:true,
25+
location: [], //the possible values for the array are: 'KTH Campus', 'KTH Kista', 'AlbaNova', 'KTH Flemingsberg', 'KTH Solna', 'KTH Södertälje', 'Handelshögskolan', 'KI Solna', 'Stockholms universitet', 'KONSTFACK'
26+
applyCreditsFilter:true,
27+
creditMin: 0,
28+
creditMax: 45,
29+
applyDepartmentFilter:false,
30+
department: []
31+
},
932

1033
setUser(user) {
1134
if (!this.user)
@@ -16,6 +39,14 @@ export const model = {
1639
this.currentSearch = searchResults;
1740
},
1841

42+
setCurrentSearchText(text){
43+
this.currentSearchText = text;
44+
},
45+
46+
setScrollPosition(position) {
47+
this.scrollPosition = position;
48+
},
49+
1950
setCourses(courses){
2051
this.courses = courses;
2152
},
@@ -68,7 +99,6 @@ export const model = {
6899
this.addCourse(course);
69100
});
70101
},
71-
72102
//for reviews
73103
async addReview(courseCode, review) {
74104
try {
@@ -87,4 +117,53 @@ export const model = {
87117
return [];
88118
}
89119
},
120+
//for filters
121+
122+
setFiltersChange() {
123+
this.filtersChange = true;
124+
},
125+
126+
setFiltersCalculated() {
127+
this.filtersCalculated = true;
128+
},
129+
130+
updateLevelFilter(level) {
131+
this.filterOptions.level = level;
132+
},
133+
updateLanguageFilter(languages) {
134+
this.filterOptions.language = languages;
135+
},
136+
updateLocationFilter(location) {
137+
this.filterOptions.location = location;
138+
},
139+
updateCreditsFilter(creditLimits) {
140+
this.filterOptions.creditMin = creditLimits[0];
141+
this.filterOptions.creditMax = creditLimits[1];
142+
},
143+
updateTranscriptElegibilityFilter(eligibility) {
144+
this.filterOptions.eligibility = eligibility;
145+
},
146+
147+
//setters for the filter options
148+
setApplyTranscriptFilter(transcriptFilterState) {
149+
this.filterOptions.applyTranscriptFilter = transcriptFilterState;
150+
},
151+
setApplyLevelFilter(levelFilterState) {
152+
this.filterOptions.applyLevelFilter = levelFilterState;
153+
},
154+
setApplyLanguageFilter(languageFilterState) {
155+
this.filterOptions.applyLanguageFilter = languageFilterState;
156+
},
157+
setApplyLocationFilter(locationFilterState) {
158+
this.filterOptions.applyLocationFilter = locationFilterState;
159+
},
160+
setApplyCreditsFilter(creditsFilterState) {
161+
this.filterOptions.applyCreditsFilter = creditsFilterState;
162+
},
163+
// setApplyDepartmentFilter(departmentFilterState) {
164+
// this.filterOptions.applyDepartmentFilter = departmentFilterState;
165+
// },
166+
167+
168+
90169
};

0 commit comments

Comments
 (0)