Skip to content

Commit 20a5ab3

Browse files
committed
Merge branch 'main' of github.com:jkluge/Find-My-Next-Course into build
2 parents a402742 + d2ed7ca commit 20a5ab3

55 files changed

Lines changed: 150156 additions & 14265 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.DS_Store

0 Bytes
Binary file not shown.

.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
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=false 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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Docker CI
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

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
FROM node:18-alpine
1+
FROM node:20-alpine
22
WORKDIR /app
33
COPY my-app .
44
RUN npm ci
55
ARG CI=false
66
RUN if [ "$CI" = "true" ]; then npm run build; fi
7+
RUN if [ "$CI" = "true" ]; then npm run lint; fi
78
EXPOSE 5173
89
CMD ["npm", "run", "dev"]

my-app/eslint.config.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ export default [
99
files: ['**/*.{js,jsx}'],
1010
languageOptions: {
1111
ecmaVersion: 2020,
12-
globals: globals.browser,
12+
globals: {
13+
...globals.browser,
14+
...globals.node,
15+
},
1316
parserOptions: {
1417
ecmaVersion: 'latest',
1518
ecmaFeatures: { jsx: true },
@@ -30,4 +33,12 @@ export default [
3033
],
3134
},
3235
},
36+
{
37+
files: ['**/tailwind.config.js'], // ✅ Ensure Node.js globals for config files
38+
languageOptions: {
39+
globals: {
40+
...globals.node,
41+
},
42+
},
43+
},
3344
]

my-app/firebase.js

Lines changed: 169 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import { initializeApp } from "firebase/app";
22
import { getAuth, GoogleAuthProvider, onAuthStateChanged } from "firebase/auth";
3-
import { get, getDatabase, ref, set, serverTimestamp } from "firebase/database";
4-
3+
import { get, getDatabase, ref, set, onValue, push } from "firebase/database";
4+
import { reaction, toJS } from "mobx";
55
// Your web app's Firebase configuration
66
const firebaseConfig = {
7-
apiKey: "AIzaSyCBckVI9nhAP62u5jZJW3F4SLulUv7znis",
8-
authDomain: "findmynextcourse.firebaseapp.com",
9-
databaseURL: "https://findmynextcourse-default-rtdb.europe-west1.firebasedatabase.app",
10-
projectId: "findmynextcourse",
11-
storageBucket: "findmynextcourse.firebasestorage.app",
12-
messagingSenderId: "893484115963",
13-
appId: "1:893484115963:web:59ac087d280dec919ccd5e"
14-
};
15-
7+
apiKey: "AIzaSyCBckVI9nhAP62u5jZJW3F4SLulUv7znis",
8+
authDomain: "findmynextcourse.firebaseapp.com",
9+
databaseURL:
10+
"https://findmynextcourse-default-rtdb.europe-west1.firebasedatabase.app",
11+
projectId: "findmynextcourse",
12+
storageBucket: "findmynextcourse.firebasestorage.app",
13+
messagingSenderId: "893484115963",
14+
appId: "1:893484115963:web:59ac087d280dec919ccd5e",
15+
};
1616

1717
// Initialize Firebase
1818
const app = initializeApp(firebaseConfig);
@@ -21,77 +21,183 @@ export const db = getDatabase(app);
2121
export const googleProvider = new GoogleAuthProvider();
2222
googleProvider.addScope("profile");
2323
googleProvider.addScope("email");
24+
let noUpload = false;
25+
26+
export function connectToFirebase(model) {
27+
loadCoursesFromCacheOrFirebase(model);
28+
onAuthStateChanged(auth, (user) => {
29+
if (user) {
30+
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+
} else {
34+
model.setUser(null); // If no user, clear user-specific data
35+
}
36+
});
37+
}
2438

2539
// fetches all relevant information to create the model
2640
async function firebaseToModel(model) {
27-
const courses = await fetchAllCourses();
28-
model.setCourses(courses);
41+
if (!model.user)
42+
return;
43+
const userRef = ref(db, `users/${model.user.uid}`);
44+
onValue(userRef, (snapshot) => {
45+
if (!snapshot.exists())
46+
return;
47+
const data = snapshot.val();
48+
noUpload = true;
49+
if (data.favourites)
50+
model.setFavourite(data.favourites);
51+
// if (data.currentSearch)
52+
// model.setCurrentSearch(data.currentSearch);
53+
noUpload = false;
54+
});
2955
}
3056

31-
export function connectToFirebase(model) {
32-
onAuthStateChanged(auth, (user) => {
33-
model.setUser(user);
34-
});
35-
firebaseToModel(model);
57+
58+
export function syncModelToFirebase(model) {
59+
reaction(
60+
() => ({
61+
userId: model?.user.uid,
62+
favourites: toJS(model.favourites),
63+
// currentSearch: toJS(model.currentSearch),
64+
// Add more per-user attributes here
65+
}),
66+
// eslint-disable-next-line no-unused-vars
67+
({ userId, favourites, currentSearch }) => {
68+
if (noUpload || !userId)
69+
return;
70+
const userRef = ref(db, `users/${userId}`);
71+
const dataToSync = {
72+
favourites,
73+
//currentSearch,
74+
};
75+
76+
set(userRef, dataToSync)
77+
.then(() => console.log("User model synced to Firebase"))
78+
.catch(console.error);
79+
}
80+
);
3681
}
3782

38-
export async function addCourse(course){
39-
if(!course?.code)
40-
return;
41-
const myRef = ref(db, `courses/${course.code}`);
42-
await set(myRef, course);
83+
function saveCoursesInChunks(courses, timestamp) {
84+
const parts = 3; // Adjust this based on course size
85+
const chunkSize = Math.ceil(courses.length / parts);
86+
87+
for (let i = 0; i < parts; i++) {
88+
const chunk = courses.slice(i * chunkSize, (i + 1) * chunkSize);
89+
localStorage.setItem(`coursesPart${i}`, JSON.stringify(chunk));
90+
}
91+
localStorage.setItem("coursesMetadata", JSON.stringify({ parts, timestamp }));
4392
}
4493

45-
export async function fetchAllCourses() {
46-
const myRef = ref(db, `courses`);
47-
const snapshot = await get(myRef);
94+
async function updateLastUpdatedTimestamp() {
95+
const timestampRef = ref(db, "metadata/lastUpdated");
96+
await set(timestampRef, Date.now());
97+
}
98+
99+
async function fetchLastUpdatedTimestamp() {
100+
const timestampRef = ref(db, "metadata/lastUpdated");
101+
const snapshot = await get(timestampRef);
102+
return snapshot.exists() ? snapshot.val() : 0;
103+
}
48104

49-
if (!snapshot.exists()) return [];
105+
export async function addCourse(course) {
106+
if (!course?.code) return;
107+
const myRef = ref(db, `courses/${course.code}`);
108+
await set(myRef, course);
109+
updateLastUpdatedTimestamp();
110+
}
50111

51-
const value = snapshot.val(); // Firebase returns an object where keys are course IDs
52-
const courses = [];
112+
export async function fetchAllCourses() {
113+
const myRef = ref(db, `courses`);
114+
const snapshot = await get(myRef);
115+
if (!snapshot.exists()) return [];
53116

54-
for (const id of Object.keys(value)) {
55-
courses.push({ id, ...value[id] });
56-
}
117+
const value = snapshot.val(); // Firebase returns an object where keys are course IDs
118+
const courses = [];
57119

58-
return courses;
120+
for (const id of Object.keys(value)) {
121+
courses.push({ id, ...value[id] });
122+
}
123+
return courses;
59124
}
60125

61-
// Before: [ {courseCode: "CS101", name: "Intro to CS"}, {...} ]
62-
// After: { "CS101": { name: "Intro to CS" }, "CS102": {...} }
126+
async function loadCoursesFromCacheOrFirebase(model) {
127+
// Load metadata from localStorage
128+
const cachedMetadata = JSON.parse(localStorage.getItem("coursesMetadata"));
129+
const firebaseTimestamp = await fetchLastUpdatedTimestamp();
130+
// check if up to date
131+
if (cachedMetadata && cachedMetadata.timestamp === firebaseTimestamp) {
132+
console.log("Using cached courses...");
133+
let mergedCourses = [];
134+
for (let i = 0; i < cachedMetadata.parts; i++) {
135+
const part = JSON.parse(localStorage.getItem(`coursesPart${i}`));
136+
if (part) mergedCourses = mergedCourses.concat(part);
137+
}
138+
model.setCourses(mergedCourses);
139+
return;
140+
}
141+
142+
// Fetch if outdated or missing
143+
console.log("Fetching courses from Firebase...");
144+
const courses = await fetchAllCourses();
145+
model.setCourses(courses);
146+
saveCoursesInChunks(courses, firebaseTimestamp);
147+
}
63148

149+
export async function saveJSONCoursesToFirebase(model, data) {
150+
if (!data || !model) {
151+
console.log("no model or data");
152+
return;
153+
}
154+
const entries = Object.entries(data);
155+
entries.forEach((entry) => {
156+
const course = {
157+
code: entry[1].code,
158+
name: entry[1]?.name ?? "",
159+
location: entry[1]?.location ?? "",
160+
department: entry[1]?.department ?? "",
161+
language: entry[1]?.language ?? "",
162+
description: entry[1]?.description ?? "",
163+
academicLevel: entry[1]?.academic_level ?? "",
164+
period: entry[1]?.period ?? "",
165+
credits: entry[1]?.credits ?? 0,
166+
//lectureCount:entry[1].courseLectureCount,
167+
//prerequisites:entry.coursePrerequisites
168+
};
169+
model.addCourse(course);
170+
});
171+
}
64172

65-
export async function fetchCoursesSnapshot() {
66-
const myRef = ref(db, `courses`);
67-
const snapshot = await get(myRef);
68-
if (!snapshot.exists()) return {}; // Return empty object instead of array
69173

70-
return snapshot.val(); // Return the object directly
174+
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);
181+
}
71182
}
72183

73-
export async function saveJSONCoursesToFirebase(model, data){
74-
if(!data || !model){
75-
console.log("no model or data")
76-
return;
77-
}
78-
const entries = Object.entries(data);
79-
entries.forEach(entry => {
80-
const course = {
81-
code : entry[1].code ,
82-
name: entry[1]?.name ?? "",
83-
location: entry[1]?.location ?? "",
84-
department: entry[1]?.department ?? "",
85-
language: entry[1]?.language ?? "",
86-
description: entry[1]?.description ?? "",
87-
academicLevel: entry[1]?.academic_level ?? "",
88-
period: entry[1]?.period ?? "",
89-
credits: entry[1]?.credits ?? 0,
90-
//lectureCount:entry[1].courseLectureCount,
91-
//prerequisites:entry.coursePrerequisites
92-
}
93-
model.addCourse(course);
94-
95-
});
184+
185+
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,
194+
...childSnapshot.val()
195+
});
196+
});
197+
return reviews;
96198
}
97199

200+
201+
202+
203+

my-app/index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
66
<link href="/src/styles.css" rel="stylesheet">
7+
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
8+
<link rel="preconnect" href="https://fonts.googleapis.com">
9+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10+
<link href="https://fonts.googleapis.com/css2?family=Kanit:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
711
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
812
<title>Find my course</title>
913
</head>

0 commit comments

Comments
 (0)