Skip to content

Commit 8e5e16d

Browse files
committed
Basic comment function to each reviews
1 parent c6bd518 commit 8e5e16d

5 files changed

Lines changed: 240 additions & 111 deletions

File tree

my-app/firebase.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getAuth, GoogleAuthProvider, onAuthStateChanged } from "firebase/auth";
33
import { getFunctions, httpsCallable } from 'firebase/functions';
44
import { get, getDatabase, ref, set, onValue, onChildRemoved, onChildAdded, runTransaction } from "firebase/database";
55
import { reaction, toJS } from "mobx";
6+
import { push } from "firebase/database";
67

78
// Your web app's Firebase configuration
89
const firebaseConfig = {
@@ -368,3 +369,33 @@ export async function getReviewsForCourse(courseCode) {
368369
});
369370
return reviews;
370371
}
372+
/**
373+
* Add a comment to a specific review
374+
* @param {string} courseCode - Course code (e.g. "A11HIB")
375+
* @param {string} reviewUserId - The user ID of the person who wrote the main review
376+
* @param {Object} commentObj - Object with { userName, text, timestamp }
377+
*/
378+
export async function addCommentToReview(courseCode, reviewUserId, commentObj) {
379+
const commentsRef = ref(db, `reviews/${courseCode}/${reviewUserId}/comments`);
380+
await push(commentsRef, commentObj);
381+
}
382+
383+
/**
384+
* Get comments for a specific review
385+
* @param {string} courseCode
386+
* @param {string} reviewUserId
387+
* @returns {Promise<Array<Object>>} Array of comments: { id, userName, text, timestamp }
388+
*/
389+
export async function getCommentsForReview(courseCode, reviewUserId) {
390+
const commentsRef = ref(db, `reviews/${courseCode}/${reviewUserId}/comments`);
391+
const snapshot = await get(commentsRef);
392+
if (!snapshot.exists()) return [];
393+
const comments = [];
394+
snapshot.forEach((childSnapshot) => {
395+
comments.push({
396+
id: childSnapshot.key,
397+
...childSnapshot.val()
398+
});
399+
});
400+
return comments;
401+
}

my-app/firebase_rules.json

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,52 @@
11
{
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": {
2+
"rules": {
3+
// Courses and Metadata
4+
"courses": {
135
".read": true,
14-
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
15-
},
16-
"locations": {
6+
".write": "auth != null && auth.uid === 'adminuid'"
7+
},
8+
"metadata": {
9+
".read": true,
10+
".write": "auth != null && auth.uid === 'adminuid'"
11+
},
12+
"departments": {
13+
".read": true,
14+
".write": "auth != null && auth.uid === 'adminuid'"
15+
},
16+
"locations": {
17+
".read": true,
18+
".write": "auth != null && auth.uid === 'adminuid'"
19+
},
20+
21+
// Reviews and Comments
22+
"reviews": {
1723
".read": true,
18-
".write": "auth != null && (auth.uid === 'adminuid' || auth.uid === 'adminuid')"
19-
},
20-
21-
// Reviews
22-
"reviews": {
23-
".read":true,
2424
"$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": {
3625
"$userID": {
37-
".read": "auth != null && auth.uid === $userID",
38-
".write": "auth != null && auth.uid === $userID"
26+
// Only the review owner can write the main review fields (not including comments)
27+
".write": "auth != null && (auth.uid === $userID)",
28+
29+
// Allow anyone to write a comment
30+
"comments": {
31+
".read": true,
32+
"$commentId": {
33+
".write": "auth != null",
34+
".validate": "newData.hasChildren(['userName', 'text', 'timestamp']) &&
35+
newData.child('userName').isString() &&
36+
newData.child('text').isString() &&
37+
newData.child('timestamp').isNumber()"
38+
}
39+
}
3940
}
4041
}
42+
},
43+
44+
// Users
45+
"users": {
46+
"$userID": {
47+
".read": "auth != null && auth.uid === $userID",
48+
".write": "auth != null && auth.uid === $userID"
49+
}
4150
}
42-
}
51+
}
52+
}

my-app/src/model.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,18 @@ export const model = {
190190

191191
async getReviews(courseCode) {
192192
try {
193-
return await getReviewsForCourse(courseCode);
193+
const rawReviews = await getReviewsForCourse(courseCode);
194+
if (!Array.isArray(rawReviews)) return [];
195+
196+
const enriched = rawReviews.map((review) => {
197+
return {
198+
...review,
199+
uid: review.uid || review.id || "",
200+
courseCode: courseCode || "",
201+
};
202+
});
203+
204+
return enriched;
194205
} catch (error) {
195206
console.error("Error fetching reviews:", error);
196207
return [];
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React, { useState } from "react";
2+
import RatingComponent from "./RatingComponent.jsx";
3+
import { model } from "../../model.js"; // Adjust the path if needed
4+
import { addReviewForCourse } from "../../firebase"; // Adjust the path if needed
5+
6+
function CommentTree({ courseCode, comment, level = 0 }) {
7+
const [showReply, setShowReply] = useState(false);
8+
const [replyText, setReplyText] = useState("");
9+
10+
const handleReplySubmit = async () => {
11+
if (replyText.trim().length === 0) return;
12+
13+
const reply = {
14+
userName: model.user?.displayName || "Anonymous",
15+
text: replyText,
16+
timestamp: Date.now(),
17+
overallRating: 0,
18+
difficultyRating: 0,
19+
professorRating: 0,
20+
professorName: "",
21+
grade: "",
22+
recommend: null,
23+
};
24+
25+
await addReviewForCourse(courseCode, reply, comment.id);
26+
window.location.reload(); // quick reload for now; optional optimization later
27+
};
28+
29+
return (
30+
<div className="ml-4 mt-4 border-l pl-4 border-gray-300">
31+
<div className="bg-gray-50 p-3 rounded-md shadow-sm">
32+
<div className="flex justify-between items-center mb-1">
33+
<p className="font-semibold text-gray-800">{comment.userName}</p>
34+
<p className="text-xs text-gray-500">
35+
{new Date(comment.timestamp).toLocaleDateString()}
36+
</p>
37+
</div>
38+
39+
<p className="text-sm text-gray-700 mb-1">{comment.text}</p>
40+
41+
<button
42+
className="text-blue-500 text-sm hover:underline"
43+
onClick={() => setShowReply(!showReply)}
44+
>
45+
{showReply ? "Cancel" : "Reply"}
46+
</button>
47+
48+
{showReply && (
49+
<div className="mt-2">
50+
<textarea
51+
className="w-full border border-gray-300 rounded-md p-2 text-sm"
52+
placeholder="Write your reply..."
53+
value={replyText}
54+
onChange={(e) => setReplyText(e.target.value)}
55+
/>
56+
<button
57+
onClick={handleReplySubmit}
58+
className="mt-1 px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700"
59+
>
60+
Post Reply
61+
</button>
62+
</div>
63+
)}
64+
</div>
65+
66+
{/* Recursive rendering of replies */}
67+
{comment.replies && comment.replies.length > 0 && (
68+
<div className="mt-2 space-y-2">
69+
{comment.replies.map((child) => (
70+
<CommentTree
71+
key={child.id}
72+
courseCode={courseCode}
73+
comment={child}
74+
level={level + 1}
75+
/>
76+
))}
77+
</div>
78+
)}
79+
</div>
80+
);
81+
}
82+
83+
export default CommentTree;

0 commit comments

Comments
 (0)