Skip to content

Commit 2753f6f

Browse files
authored
Anonymous function to review and comment (#146)
* Basic comment function to each reviews * anonymous function to review and comments return
1 parent 1d13ecf commit 2753f6f

File tree

6 files changed

+344
-147
lines changed

6 files changed

+344
-147
lines changed

my-app/firebase.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
runTransaction,
1313
} from "firebase/database";
1414
import { reaction, toJS } from "mobx";
15+
import { push } from "firebase/database";
1516

1617
/**
1718
* Firebase configuration and initialization.
@@ -467,3 +468,33 @@ export async function getReviewsForCourse(courseCode) {
467468
});
468469
return reviews;
469470
}
471+
/**
472+
* Add a comment to a specific review
473+
* @param {string} courseCode - Course code (e.g. "A11HIB")
474+
* @param {string} reviewUserId - The user ID of the person who wrote the main review
475+
* @param {Object} commentObj - Object with { userName, text, timestamp }
476+
*/
477+
export async function addCommentToReview(courseCode, reviewUserId, commentObj) {
478+
const commentsRef = ref(db, `reviews/${courseCode}/${reviewUserId}/comments`);
479+
await push(commentsRef, commentObj);
480+
}
481+
482+
/**
483+
* Get comments for a specific review
484+
* @param {string} courseCode
485+
* @param {string} reviewUserId
486+
* @returns {Promise<Array<Object>>} Array of comments: { id, userName, text, timestamp }
487+
*/
488+
export async function getCommentsForReview(courseCode, reviewUserId) {
489+
const commentsRef = ref(db, `reviews/${courseCode}/${reviewUserId}/comments`);
490+
const snapshot = await get(commentsRef);
491+
if (!snapshot.exists()) return [];
492+
const comments = [];
493+
snapshot.forEach((childSnapshot) => {
494+
comments.push({
495+
id: childSnapshot.key,
496+
...childSnapshot.val()
497+
});
498+
});
499+
return comments;
500+
}

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
@@ -201,7 +201,18 @@ export const model = {
201201

202202
async getReviews(courseCode) {
203203
try {
204-
return await getReviewsForCourse(courseCode);
204+
const rawReviews = await getReviewsForCourse(courseCode);
205+
if (!Array.isArray(rawReviews)) return [];
206+
207+
const enriched = rawReviews.map((review) => {
208+
return {
209+
...review,
210+
uid: review.uid || review.id || "",
211+
courseCode: courseCode || "",
212+
};
213+
});
214+
215+
return enriched;
205216
} catch (error) {
206217
console.error("Error fetching reviews:", error);
207218
return [];

my-app/src/presenters/ReviewPresenter.jsx

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { ReviewView } from '../views/ReviewView.jsx';
77
*/
88
export const ReviewPresenter = observer(({ model, course }) => {
99
const [reviews, setReviews] = useState([]);
10-
const [postAnonymous, setAnonymous] = useState(false);
1110
const [errorMessage, setErrorMessage] = useState("");
11+
const [anonState, setAnonState] = useState(false);
1212
const [formData, setFormData] = useState({
1313
text: "",
1414
overallRating: 0,
@@ -20,7 +20,7 @@ export const ReviewPresenter = observer(({ model, course }) => {
2020
avgRating: 0,
2121
});
2222

23-
// fetch reviews when the current course code or model updates.
23+
// Fetch reviews when the current course code or model updates
2424
useEffect(() => {
2525
async function fetchReviews() {
2626
const data = await model.getReviews(course.code);
@@ -29,25 +29,25 @@ export const ReviewPresenter = observer(({ model, course }) => {
2929
fetchReviews();
3030
}, [course.code, model]);
3131

32-
/**
33-
* Set an error message if the user is not logged in or posted already.
34-
*/
32+
const hasPreviousReview = !!model?.user?.uid && reviews.some(r => r.uid === model.user.uid);
33+
34+
// Set error message based on login state or review existence
3535
useEffect(() => {
36-
async function updateError() {
37-
if(!model?.user?.uid)
38-
setErrorMessage("You need to be logged in to post a comment - Posting anonymously is possible.");
39-
else if(reviews.filter((review)=>{return review.uid == model?.user?.uid}).length > 0)
40-
setErrorMessage("Everyone can only post once. Submitting a new comment will replace the old one.");
36+
if (!model?.user?.uid) {
37+
setErrorMessage("You need to be logged in to post a review - Posting anonymously is possible.");
38+
} else if (hasPreviousReview) {
39+
setErrorMessage("Everyone can only post once. Submitting a new review will replace the old one.");
40+
} else {
41+
setErrorMessage("");
4142
}
42-
updateError();
43-
}, [reviews, model?.user?.uid]);
43+
}, [reviews, model?.user?.uid, hasPreviousReview]);
4444

4545
/**
46-
* Handle the submssion of a review and set errors if needed.
47-
* @returns void
46+
* Handle the submission of a review and set errors if needed.
47+
* @param {boolean} anon - whether to post anonymously
4848
*/
49-
const handleReviewSubmit = async () => {
50-
if(!model?.user){
49+
const handleReviewSubmit = async (anon) => {
50+
if (!model?.user) {
5151
setErrorMessage("You need to be logged in to post a comment - Posting anonymously is possible.");
5252
return;
5353
}
@@ -57,32 +57,34 @@ export const ReviewPresenter = observer(({ model, course }) => {
5757
return;
5858
}
5959

60-
// create the post object - look into firebase rules if you want to change this.
6160
const review = {
62-
userName: postAnonymous ? "Anonymous" : model.user?.displayName,
61+
userName: anon ? "Anonymous" : model.user?.displayName,
6362
uid: model?.user?.uid,
6463
timestamp: Date.now(),
6564
...formData,
6665
};
67-
68-
if(!await model.addReview(course.code, review)){
69-
setErrorMessage("Something went wrong when posting. Are you logged in?")
66+
67+
const success = await model.addReview(course.code, review);
68+
if (!success) {
69+
setErrorMessage("Something went wrong when posting. Are you logged in?");
7070
return;
7171
}
72-
// refetch after submission
72+
7373
const updatedReviews = await model.getReviews(course.code);
7474
setReviews(updatedReviews);
75+
7576
setFormData({
7677
text: "",
7778
overallRating: 0,
7879
difficultyRating: 0,
7980
professorName: "",
81+
professorRating: 0,
8082
grade: "",
81-
recommended: false,
83+
recommend: null,
84+
avgRating: 0,
8285
});
8386
};
8487

85-
8688
return (
8789
<ReviewView
8890
course={course}
@@ -92,9 +94,9 @@ export const ReviewPresenter = observer(({ model, course }) => {
9294
handleReviewSubmit={handleReviewSubmit}
9395
errorMessage={errorMessage}
9496
setErrorMessage={setErrorMessage}
95-
postAnonymous={postAnonymous}
96-
setAnonymous={setAnonymous}
97+
hasPreviousReview={hasPreviousReview}
98+
anonState={anonState}
99+
setAnonState={setAnonState}
97100
/>
98-
99101
);
100102
});
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)