diff --git a/tcf_website/migrations/0024_question_title.py b/tcf_website/migrations/0024_question_title.py
new file mode 100644
index 000000000..aa8e55a37
--- /dev/null
+++ b/tcf_website/migrations/0024_question_title.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.28 on 2026-02-04 05:13
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("tcf_website", "0023_remove_sectionenrollment_section_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="question",
+ name="title",
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ ]
diff --git a/tcf_website/migrations/0025_question_instructor_optional.py b/tcf_website/migrations/0025_question_instructor_optional.py
new file mode 100644
index 000000000..65ee0f0f5
--- /dev/null
+++ b/tcf_website/migrations/0025_question_instructor_optional.py
@@ -0,0 +1,25 @@
+# Generated manually
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("tcf_website", "0024_question_title"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="question",
+ name="instructor",
+ field=models.ForeignKey(
+ blank=True,
+ default=None,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="tcf_website.instructor",
+ ),
+ ),
+ ]
diff --git a/tcf_website/migrations/0026_answer_parent_answer.py b/tcf_website/migrations/0026_answer_parent_answer.py
new file mode 100644
index 000000000..1e78cfc79
--- /dev/null
+++ b/tcf_website/migrations/0026_answer_parent_answer.py
@@ -0,0 +1,25 @@
+# Generated by Django 4.2.28 on 2026-03-04 00:02
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("tcf_website", "0025_question_instructor_optional"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="answer",
+ name="parent_answer",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="replies",
+ to="tcf_website.answer",
+ ),
+ ),
+ ]
diff --git a/tcf_website/migrations/0027_remove_answer_unique_answer_per_user_and_question_and_more.py b/tcf_website/migrations/0027_remove_answer_unique_answer_per_user_and_question_and_more.py
new file mode 100644
index 000000000..aac3a47ee
--- /dev/null
+++ b/tcf_website/migrations/0027_remove_answer_unique_answer_per_user_and_question_and_more.py
@@ -0,0 +1,25 @@
+# Generated by Django 4.2.28 on 2026-03-04 00:11
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("tcf_website", "0026_answer_parent_answer"),
+ ]
+
+ operations = [
+ migrations.RemoveConstraint(
+ model_name="answer",
+ name="unique answer per user and question",
+ ),
+ migrations.AddConstraint(
+ model_name="answer",
+ constraint=models.UniqueConstraint(
+ condition=models.Q(("parent_answer__isnull", True)),
+ fields=("user", "question"),
+ name="unique answer per user and question",
+ ),
+ ),
+ ]
diff --git a/tcf_website/migrations/0028_question_department_and_optional_course.py b/tcf_website/migrations/0028_question_department_and_optional_course.py
new file mode 100644
index 000000000..22285473a
--- /dev/null
+++ b/tcf_website/migrations/0028_question_department_and_optional_course.py
@@ -0,0 +1,36 @@
+# Generated manually
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("tcf_website", "0027_remove_answer_unique_answer_per_user_and_question_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="question",
+ name="department",
+ field=models.ForeignKey(
+ blank=True,
+ default=None,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="tcf_website.department",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="question",
+ name="course",
+ field=models.ForeignKey(
+ blank=True,
+ default=None,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="tcf_website.course",
+ ),
+ ),
+ ]
diff --git a/tcf_website/migrations/0029_alter_answer_semester.py b/tcf_website/migrations/0029_alter_answer_semester.py
new file mode 100644
index 000000000..cc2f7358a
--- /dev/null
+++ b/tcf_website/migrations/0029_alter_answer_semester.py
@@ -0,0 +1,22 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("tcf_website", "0028_question_department_and_optional_course"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="answer",
+ name="semester",
+ field=models.ForeignKey(
+ blank=True,
+ default=None,
+ null=True,
+ on_delete=models.SET_NULL,
+ to="tcf_website.semester",
+ ),
+ ),
+ ]
diff --git a/tcf_website/migrations/0030_remove_answer_unique_answer_per_user_and_question.py b/tcf_website/migrations/0030_remove_answer_unique_answer_per_user_and_question.py
new file mode 100644
index 000000000..ac03b8a57
--- /dev/null
+++ b/tcf_website/migrations/0030_remove_answer_unique_answer_per_user_and_question.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.28 on 2026-05-01
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("tcf_website", "0029_alter_answer_semester"),
+ ]
+
+ operations = [
+ migrations.RemoveConstraint(
+ model_name="answer",
+ name="unique answer per user and question",
+ ),
+ ]
diff --git a/tcf_website/models/models.py b/tcf_website/models/models.py
index 73f4fcee3..01740cd50 100644
--- a/tcf_website/models/models.py
+++ b/tcf_website/models/models.py
@@ -1,6 +1,7 @@
# pylint: disable=missing-class-docstring, wildcard-import, fixme, too-many-lines
"""TCF Database models."""
+from collections import defaultdict
from decimal import Decimal
from django.conf import settings
@@ -14,6 +15,7 @@
Avg,
Case,
CharField,
+ Count,
Exists,
ExpressionWrapper,
F,
@@ -1421,17 +1423,40 @@ class Meta:
class Question(models.Model):
"""Question model.
Belongs to a User.
- Has a course and instructor.
+ Has either a course or department target, and an optional instructor.
"""
+ title = models.CharField(max_length=200, blank=True)
text = models.TextField()
- course = models.ForeignKey(Course, on_delete=models.CASCADE)
- instructor = models.ForeignKey(Instructor, on_delete=models.CASCADE, default=None)
+ course = models.ForeignKey(
+ Course, on_delete=models.SET_NULL, null=True, blank=True, default=None
+ )
+ department = models.ForeignKey(
+ Department, on_delete=models.SET_NULL, null=True, blank=True, default=None
+ )
+ instructor = models.ForeignKey(
+ Instructor, on_delete=models.SET_NULL, null=True, blank=True, default=None
+ )
user = models.ForeignKey(User, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
+ @property
+ def is_deleted(self):
+ """Return whether this question has been soft-deleted."""
+ return self.text == ""
+
+ def soft_delete(self):
+ """Clear user-authored content while preserving replies."""
+ self.title = ""
+ self.text = ""
+ self.save(update_fields=["title", "text"])
+
def __str__(self):
- return f"Question for {self.course}"
+ if self.course:
+ return f"Question for {self.course}"
+ if self.department:
+ return f"Question for {self.department.name}"
+ return "Question"
def count_votes(self):
"""Sum votes for review."""
@@ -1466,38 +1491,11 @@ def upvote(self, user):
question=self,
)
- def downvote(self, user):
- """Create a downvote."""
-
- # Check if already downvoted.
- downvoted = VoteQuestion.objects.filter(
- user=user,
- question=self,
- value=-1,
- ).exists()
-
- # Delete all prior votes.
- VoteQuestion.objects.filter(
- user=user,
- question=self,
- ).delete()
-
- # Don't downvote again if previously downvoted.
- if downvoted:
- return
-
- VoteQuestion.objects.create(
- value=-1,
- user=user,
- question=self,
- )
-
@staticmethod
def display_activity(course_id, instructor_id, user):
"""Prepare review list for course-instructor page."""
question = (
Question.objects.filter(instructor=instructor_id, course=course_id)
- .exclude(text="")
.annotate(
sum_q_votes=models.functions.Coalesce(
models.Sum("votequestion__value"), models.Value(0)
@@ -1521,13 +1519,29 @@ class Answer(models.Model):
"""Answer model.
Belongs to a User.
Has a question.
+ Can have a parent answer for replies.
"""
text = models.TextField()
question = models.ForeignKey(Question, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
- semester = models.ForeignKey(Semester, on_delete=models.CASCADE, default=None)
+ semester = models.ForeignKey(
+ Semester, on_delete=models.SET_NULL, null=True, blank=True, default=None
+ )
+ parent_answer = models.ForeignKey(
+ "self", null=True, blank=True, on_delete=models.CASCADE, related_name="replies"
+ )
+
+ @property
+ def is_deleted(self):
+ """Return whether this answer has been soft-deleted."""
+ return self.text == ""
+
+ def soft_delete(self):
+ """Clear user-authored content while preserving nested replies."""
+ self.text = ""
+ self.save(update_fields=["text"])
def __str__(self):
return f"Answer for {self.question}"
@@ -1565,38 +1579,12 @@ def upvote(self, user):
answer=self,
)
- def downvote(self, user):
- """Create a downvote."""
-
- # Check if already downvoted.
- downvoted = VoteAnswer.objects.filter(
- user=user,
- answer=self,
- value=-1,
- ).exists()
-
- # Delete all prior votes.
- VoteAnswer.objects.filter(
- user=user,
- answer=self,
- ).delete()
-
- # Don't downvote again if previously downvoted.
- if downvoted:
- return
-
- VoteAnswer.objects.create(
- value=-1,
- user=user,
- answer=self,
- )
-
@staticmethod
def display_activity(question_id, user):
- """Prepare answers for course-instructor page."""
- answer = (
+ """Prepare answers and replies for question detail page."""
+ answers = (
Answer.objects.filter(question=question_id)
- .exclude(text="")
+ .select_related("user", "semester", "parent_answer")
.annotate(
sum_a_votes=models.functions.Coalesce(
models.Sum("voteanswer__value"), models.Value(0)
@@ -1604,7 +1592,7 @@ def display_activity(question_id, user):
)
)
if user.is_authenticated:
- answer = answer.annotate(
+ answers = answers.annotate(
user_a_vote=models.functions.Coalesce(
models.Sum(
"voteanswer__value",
@@ -1613,15 +1601,36 @@ def display_activity(question_id, user):
models.Value(0),
),
)
- return answer.order_by("-created")
- class Meta:
- constraints = [
- models.UniqueConstraint(
- fields=["user", "question"],
- name="unique answer per user and question",
- )
- ]
+ answers_list = list(answers.order_by("created"))
+ children_by_parent = defaultdict(list)
+ root_answers = []
+
+ for answer in answers_list:
+ answer.nested_replies = []
+ answer.is_deleted_placeholder = answer.is_deleted
+ if answer.parent_answer_id is None:
+ root_answers.append(answer)
+ else:
+ children_by_parent[answer.parent_answer_id].append(answer)
+
+ def attach_children(answer, depth):
+ answer.depth = depth
+ answer.render_depth = min(depth, 3)
+ children = children_by_parent.get(answer.id, [])
+ visible_children = []
+ for child in children:
+ if attach_children(child, depth + 1):
+ visible_children.append(child)
+ answer.nested_replies = visible_children
+ return bool(answer.text) or bool(answer.nested_replies)
+
+ visible_roots = []
+ for answer in root_answers:
+ if attach_children(answer, 0):
+ visible_roots.append(answer)
+
+ return sorted(visible_roots, key=lambda answer: answer.created, reverse=True)
class VoteQuestion(models.Model):
diff --git a/tcf_website/static/icons/icons.css b/tcf_website/static/icons/icons.css
index 261a94203..edbda7494 100644
--- a/tcf_website/static/icons/icons.css
+++ b/tcf_website/static/icons/icons.css
@@ -211,6 +211,15 @@
background-position: center;
}
+.fa-thumbs-up-solid {
+ width: 1em;
+ height: 1em;
+ background-image:url("../icons/img/fa-thumbs-up-solid.svg");
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-position: center;
+}
+
.fa-thumbs-down {
width: 1em;
height: 1em;
@@ -280,11 +289,6 @@
background-repeat: no-repeat;
background-size: contain;
background-position: center;
- width: 1em;
- height: 1em;
- vertical-align: middle;
- display: inline-block;
- filter: invert(100%) brightness(150%);
}
.fa-user-friends {
@@ -329,6 +333,20 @@
background-position: center;
}
+.fa-comments {
+ background-image: url("../icons/img/fa-comments.svg");
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-position: center;
+}
+
+.fa-reply {
+ background-image: url("../icons/img/fa-reply.svg");
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-position: center;
+}
+
.invert-to-light {
filter: invert(1);
}
diff --git a/tcf_website/static/icons/img/fa-comments.svg b/tcf_website/static/icons/img/fa-comments.svg
new file mode 100644
index 000000000..56534d03b
--- /dev/null
+++ b/tcf_website/static/icons/img/fa-comments.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tcf_website/static/icons/img/fa-reply.svg b/tcf_website/static/icons/img/fa-reply.svg
new file mode 100644
index 000000000..42d94b077
--- /dev/null
+++ b/tcf_website/static/icons/img/fa-reply.svg
@@ -0,0 +1,9 @@
+
+
+ Reply Streamline Icon: https://streamlinehq.com
+
+
+ reply
+
+
+
\ No newline at end of file
diff --git a/tcf_website/static/icons/img/fa-thumbs-up-solid.svg b/tcf_website/static/icons/img/fa-thumbs-up-solid.svg
new file mode 100644
index 000000000..c104d40b0
--- /dev/null
+++ b/tcf_website/static/icons/img/fa-thumbs-up-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tcf_website/static/qa/new_answer.js b/tcf_website/static/qa/new_answer.js
index 71f8c539c..cf1fc7230 100644
--- a/tcf_website/static/qa/new_answer.js
+++ b/tcf_website/static/qa/new_answer.js
@@ -44,45 +44,17 @@ jQuery(function ($) {
ids.forEach((item) => populateDropdown(item));
});
-// Below checks whether the user has already answered the question
$(document).ready(function () {
- $("#duplicate-answer").hide();
-
$(document).on("click", "#answerQuestionBtn", function () {
const questionId = $(this).data("id");
$("#answerForm #questionInput").val(questionId);
});
- // Stop normal submit to check for duplicate answer with ajax request
$("#answerForm").submit(function (e) {
- e.preventDefault();
-
- $.ajax({
- type: "POST",
- url: "/answers/check_duplicate/",
- data: $("#answerForm").serialize(),
- async: false,
- success: function (check) {
- if (check.duplicate) {
- // Display error message for warning
- $("#duplicate-answer").show();
- } else {
- // Timeout button for 3 seconds so user can't spam button while form is submitting
- $("#submitBtn").prop("disabled", true);
- setTimeout(enableButton, 3000);
- // Not a duplicate answer so proceed with normal form submission for new answer
- document.getElementById("answerForm").submit();
- }
- },
- });
+ $("#submitBtn").prop("disabled", true);
});
});
-// Re-enable button
-function enableButton() {
- $("#submitBtn").prop("disabled", false);
-}
-
// Clears all dropdown options & adds a disabled default option
function clearDropdown(id) {
$(id).empty();
diff --git a/tcf_website/static/qa/qa_dashboard.css b/tcf_website/static/qa/qa_dashboard.css
new file mode 100644
index 000000000..92540b483
--- /dev/null
+++ b/tcf_website/static/qa/qa_dashboard.css
@@ -0,0 +1,1043 @@
+/* Main Container */
+.qa-container {
+ display: flex;
+ height: calc(100vh - 120px);
+ width: 100%;
+ overflow: hidden;
+ background: #ffffff;
+}
+
+/* Left Sidebar: Posts List */
+.qa-sidebar {
+ flex: 0 0 380px;
+ background: #f8f9fa;
+ border-right: 1px solid #dee2e6;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+/* Header Section */
+.qa-header {
+ padding: 1rem;
+ background: #ffffff;
+ border-bottom: 1px solid #dee2e6;
+}
+
+.btn-new-post {
+ width: 100%;
+ background: var(--main-color);
+ color: white;
+ border: none;
+ padding: 0.75rem 1rem;
+ border-radius: 6px;
+ font-weight: 600;
+ cursor: pointer;
+ margin-bottom: 0.75rem;
+ transition: background 0.2s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.btn-new-post:hover {
+ background: var(--secondary-color);
+}
+
+.btn-new-post i {
+ margin-right: 0.5rem;
+}
+
+.search-container {
+ position: relative;
+}
+
+.search-icon {
+ position: absolute;
+ left: 0.75rem;
+ top: 50%;
+ transform: translateY(-50%);
+ color: #6c757d;
+}
+
+.search-input {
+ width: 100%;
+ padding: 0.5rem 0.75rem 0.5rem 2.5rem;
+ border: 1px solid #ced4da;
+ border-radius: 4px;
+ font-size: 0.9rem;
+}
+
+.search-input:focus {
+ outline: none;
+ border-color: var(--main-color);
+ box-shadow: 0 0 0 0.2rem rgba(39, 79, 151, 0.15);
+}
+
+/* Filters Section */
+.qa-filters {
+ padding: 0.5rem 1.25rem 0.5rem 1rem;
+ background: #ffffff;
+ border-bottom: 1px solid #dee2e6;
+ display: flex;
+ gap: 0;
+ box-sizing: border-box;
+}
+
+.filter-btn {
+ background: transparent;
+ border: 1px solid #dee2e6;
+ padding: 0.35rem 0.75rem;
+ border-radius: 4px;
+ font-size: 0.85rem;
+ cursor: pointer;
+ color: #495057;
+ transition: all 0.2s;
+}
+
+.filter-btn:hover {
+ background: #e9ecef;
+}
+
+.filter-btn.filter-active {
+ background: #e7f3ff;
+ color: var(--main-color);
+ border-color: var(--main-color);
+ font-weight: 600;
+}
+
+.filter-btn i {
+ margin-right: 0.25rem;
+}
+
+.filter-row {
+ display: flex;
+ gap: 0;
+ width: auto;
+ max-width: 100%;
+ align-items: center;
+ padding-right: 0.15rem;
+}
+
+.filter-row .dropdown {
+ flex: 0 0 auto;
+ min-width: 0;
+ position: relative;
+ z-index: 20;
+}
+
+#departmentDropdownContainer .filter-btn {
+ display: inline-flex;
+ align-items: center;
+ min-width: 0;
+ width: 180px;
+}
+
+#departmentDropdownContainer .filter-btn span {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ display: inline-block;
+}
+
+#courseDropdownContainer {
+ flex: 0 0 auto;
+ min-width: 0;
+ margin-left: -2px;
+ opacity: 0;
+ transform: translateX(-8px);
+ transition: opacity 0.2s ease, transform 0.2s ease;
+}
+
+#courseDropdownContainer .filter-btn {
+ display: inline-flex;
+ align-items: center;
+ white-space: nowrap;
+}
+
+#courseDropdownContainer .filter-btn span {
+ max-width: 18ch;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+#courseDropdownContainer.is-visible {
+ opacity: 1;
+ transform: translateX(0);
+}
+
+/* Dropdown search input */
+.dropdown-search-wrapper {
+ padding: 0.4rem 0.6rem;
+ border-bottom: 1px solid #e9ecef;
+}
+
+.dropdown-search-input {
+ width: 100%;
+ padding: 0.35rem 0.6rem;
+ border: 1px solid #ced4da;
+ border-radius: 4px;
+ font-size: 0.825rem;
+ outline: none;
+}
+
+.dropdown-search-input:focus {
+ border-color: var(--main-color);
+ box-shadow: 0 0 0 0.15rem rgba(39, 79, 151, 0.15);
+}
+
+.dropdown-items-list {
+ max-height: 250px;
+ overflow-y: auto;
+}
+
+.dropdown-items-list .dropdown-item.d-none {
+ display: none !important;
+}
+
+/* Posts List */
+.posts-list {
+ flex: 1;
+ overflow-y: auto;
+ background: #f8f9fa;
+}
+
+.post-item {
+ display: block;
+ width: 100%;
+ padding: 1rem;
+ background: white;
+ border: 0;
+ border-bottom: 1px solid #e9ecef;
+ cursor: pointer;
+ font: inherit;
+ text-align: left;
+ transition: background 0.2s;
+}
+
+.post-item:hover {
+ background: #f8f9fa;
+}
+
+.post-item.active {
+ background: #e7f3ff;
+ border-left: 3px solid var(--main-color);
+}
+
+.post-header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.post-tag {
+ background: var(--accent-color);
+ color: white;
+ padding: 0.15rem 0.5rem;
+ border-radius: 3px;
+ font-size: 0.75rem;
+ font-weight: 600;
+}
+
+.post-title {
+ font-weight: 600;
+ font-size: 0.9rem;
+ flex: 1;
+ color: #212529;
+}
+
+.post-date {
+ font-size: 0.75rem;
+ color: #6c757d;
+}
+
+.post-preview {
+ font-size: 0.85rem;
+ color: #495057;
+ margin-bottom: 0.5rem;
+ line-height: 1.4;
+}
+
+.post-footer {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+.post-stats {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3rem;
+}
+
+/* Right Content Area */
+.qa-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ background: white;
+ overflow: hidden;
+}
+
+/* Content Navigation */
+.content-nav {
+ display: flex;
+ align-items: center;
+ padding: 1rem;
+ border-bottom: 1px solid #dee2e6;
+ gap: 1rem;
+}
+
+.nav-btn {
+ background: transparent;
+ border: none;
+ color: #495057;
+ cursor: pointer;
+ padding: 0.5rem;
+ font-size: 1rem;
+ transition: color 0.2s;
+}
+
+.nav-btn:hover {
+ color: var(--accent-color);
+}
+
+.post-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.5rem;
+ color: #495057;
+ font-size: 0.9rem;
+}
+
+.post-info h3,
+.post-info .instructor h6 {
+ margin: 0;
+}
+
+.post-info h3 {
+ font-size: 1.75rem;
+ line-height: 1.2;
+}
+
+.post-info .course-link {
+ color: black;
+ text-decoration: none;
+}
+
+.post-info .course-link:hover {
+ color: var(--accent-color);
+}
+
+.post-info .instructor-profile {
+ display: inline-flex;
+ align-items: center;
+ text-decoration: none;
+}
+
+.post-info .instructor h6 {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.post-info .qa-instructor-link {
+ font-size: 1rem;
+ line-height: 1.2;
+}
+
+.post-info .instructor-select {
+ font-size: inherit;
+ line-height: 1.1;
+ color: var(--main-color)
+}
+
+.post-info .instructor-select:hover {
+ color: var(--main-color)
+}
+
+.post-info i {
+ color: #6c757d;
+}
+
+/* Post Content */
+.post-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 2rem;
+}
+
+/* Main Question Section */
+.main-question {
+ margin-bottom: 1rem;
+}
+
+.question-header-wrapper {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 1.5rem;
+ margin-bottom: 1rem;
+}
+
+.question-header-content {
+ flex: 1;
+}
+
+/* Comments Section */
+.comments-section {
+ margin-top: 0.5rem;
+}
+
+.comments-header {
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: #212529;
+ margin-bottom: 1.5rem;
+ padding-bottom: 0.75rem;
+ border-bottom: 1px solid #e9ecef;
+}
+
+/* Thread Container */
+.thread-container {
+ margin-bottom: 2rem;
+}
+
+.thread-post {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ margin-bottom: 2rem;
+ position: relative;
+ min-width: 0;
+}
+
+.thread-post.is-reply {
+ margin-bottom: 1rem;
+}
+
+.post-main {
+ flex: 1;
+ min-width: 0;
+}
+
+.post-meta {
+ color: #adb5bd;
+ font-size: 15px;
+ margin-bottom: 1rem;
+}
+
+.post-header-info {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.answer-header-left {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.answer-header-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+
+.action-btn-sm {
+ background: transparent;
+ border: none;
+ padding: 0.2rem 0.4rem;
+ cursor: pointer;
+ color: #adb5bd;
+ font-size: 0.8rem;
+ border-radius: 3px;
+ transition: all 0.2s;
+}
+
+.action-btn-sm:hover {
+ color: #495057;
+ background: #f0f0f0;
+}
+
+.action-btn-sm.delete-answer-btn:hover {
+ color: #dc3545;
+ background: #fdf0f0;
+}
+
+.semester-tag {
+ background: var(--accent-color);
+ color: white;
+ padding: 0.15rem 0.5rem;
+ border-radius: 3px;
+ font-size: 0.75rem;
+ font-weight: 600;
+}
+
+.post-time {
+ font-size: 0.875rem;
+ color: #6c757d;
+}
+
+.post-content-title {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: #212529;
+}
+
+.post-body {
+ font-size: 1rem;
+ line-height: 1.6;
+ margin-bottom: 1rem;
+ color: #333;
+}
+
+.post-body p {
+ margin-bottom: 0.75rem;
+}
+
+.post-tags {
+ margin-bottom: 1rem;
+}
+
+.tag {
+ display: inline-block;
+ background: var(--main-color);
+ color: white;
+ padding: 0.15rem 0.5rem;
+ border-radius: 3px;
+ font-size: 0.75rem;
+ margin-right: 0.5rem;
+}
+
+/* Post Actions */
+.post-actions {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-top: 0.75rem;
+ flex-wrap: wrap;
+}
+
+.main-question .post-actions {
+ padding: 1rem 0;
+ margin-top: 0.25rem;
+}
+
+.main-question .action-btn {
+ background: transparent;
+ border: 1px solid #dee2e6;
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ cursor: pointer;
+ color: #495057;
+ font-size: 0.875rem;
+ transition: all 0.2s;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ outline: none;
+}
+
+.main-question .action-btn:hover {
+ background: #f8f9fa;
+ color: var(--accent-color);
+}
+
+.main-question .action-btn.delete-question-btn:hover {
+ color: #dc3545;
+ border-color: #dc3545;
+ background: #fdf0f0;
+}
+
+.question-actions-divider {
+ width: 1px;
+ height: 1.5rem;
+ background: #dee2e6;
+ margin: 0 0.25rem;
+}
+
+.thread-container .post-actions {
+ gap: 0.25rem;
+}
+
+.comments-section .action-btn {
+ background: transparent;
+ border: none;
+ padding: 0.25rem 0.5rem;
+ cursor: pointer;
+ color: #495057;
+ font-size: 0.9rem;
+ transition: color 0.2s;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ position: relative;
+}
+
+.comments-section .action-btn:hover {
+ color: var(--main-color);
+}
+
+.comments-section .action-btn[title]:hover::after {
+ content: attr(title);
+ position: absolute;
+ bottom: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ background: #333;
+ color: white;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ white-space: nowrap;
+ margin-bottom: 4px;
+}
+
+.action-btn i {
+ font-size: 1rem;
+}
+
+/* Replies Container */
+.replies-container {
+ margin-top: 1rem;
+ margin-left: 1.25rem;
+ padding-left: 1rem;
+ border-left: 2px solid #e9ecef;
+}
+
+.replies-container.thread-depth-1 {
+ margin-left: 2rem;
+}
+
+.replies-container.thread-depth-2,
+.replies-container.thread-depth-3 {
+ margin-left: 1.5rem;
+}
+
+.replies-container.thread-depth-0 {
+ margin-left: 0;
+}
+
+.replies-container .thread-post {
+ margin-bottom: 1rem;
+}
+
+/* Reply Button */
+.reply-btn {
+ background: transparent;
+ border: none;
+ padding: 0.25rem 0.5rem;
+ cursor: pointer;
+ color: #6c757d;
+ font-size: 0.875rem;
+ transition: color 0.2s;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+}
+
+.reply-btn:hover {
+ color: var(--main-color);
+}
+
+.reply-btn i {
+ font-size: 0.9rem;
+}
+
+/* Reply Form Container */
+.reply-form-container {
+ margin-top: 1rem;
+ margin-left: 1.25rem;
+ padding-left: 1rem;
+ border-left: 2px solid var(--main-color);
+}
+
+.reply-form-container.thread-depth-0 {
+ margin-left: 0;
+}
+
+.reply-form-container.thread-depth-1 {
+ margin-left: 2rem;
+}
+
+.reply-form-container.thread-depth-2,
+.reply-form-container.thread-depth-3 {
+ margin-left: 1.5rem;
+}
+
+.reply-form-container .reply-textarea {
+ width: 100%;
+ min-height: 80px;
+ padding: 0.75rem 1rem;
+ border: 1px solid #ced4da;
+ border-radius: 6px;
+ font-size: 0.9rem;
+ font-family: inherit;
+ resize: vertical;
+ margin-bottom: 0.75rem;
+}
+
+.reply-form-container .reply-textarea:focus {
+ outline: none;
+ border-color: var(--main-color);
+ box-shadow: 0 0 0 0.2rem rgba(39, 79, 151, 0.15);
+}
+
+.btn-cancel-reply {
+ background: transparent;
+ border: 1px solid #dee2e6;
+ padding: 0.35rem 1rem;
+ border-radius: 4px;
+ font-weight: 600;
+ font-size: 0.85rem;
+ cursor: pointer;
+ color: #495057;
+ transition: all 0.2s;
+}
+
+.btn-cancel-reply:hover {
+ background: #f8f9fa;
+ border-color: #adb5bd;
+}
+
+/* Reply Input */
+.reply-input-container {
+ display: flex;
+ gap: 1rem;
+ margin-top: 2rem;
+ padding-top: 2rem;
+ border-top: 1px solid #dee2e6;
+}
+
+.reply-input-container form {
+ width: 100%;
+ flex: 1;
+}
+
+.reply-input-wrapper {
+ flex: 1;
+}
+
+.reply-textarea {
+ width: 100%;
+ min-height: 100px;
+ padding: 0.75rem 1rem;
+ border: 1px solid #ced4da;
+ border-radius: 6px;
+ font-size: 0.95rem;
+ font-family: inherit;
+ resize: vertical;
+ margin-bottom: 0.75rem;
+}
+
+.reply-textarea:focus {
+ outline: none;
+ border-color: var(--main-color);
+ box-shadow: 0 0 0 0.2rem rgba(39, 79, 151, 0.15);
+}
+
+.btn-submit-reply {
+ background: var(--main-color);
+ color: white;
+ border: none;
+ padding: 0.35rem 1rem;
+ border-radius: 4px;
+ font-weight: 600;
+ font-size: 0.85rem;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.btn-submit-reply:hover {
+ background: var(--secondary-color);
+}
+
+/* Modal Styles */
+.modal-overlay {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 1000;
+ justify-content: center;
+ align-items: center;
+}
+
+.modal-overlay.active {
+ display: flex;
+}
+
+.modal-container {
+ background: white;
+ border-radius: 8px;
+ width: 100%;
+ max-width: 900px;
+ max-height: 80vh;
+ overflow-y: auto;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.75rem 1.25rem;
+ border-bottom: 1px solid #dee2e6;
+}
+
+.modal-header h2 {
+ margin: 0;
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: #212529;
+}
+
+.modal-close {
+ background: transparent;
+ border: none;
+ font-size: 1rem;
+ color: #6c757d;
+ cursor: pointer;
+ padding: 0.15rem;
+ transition: color 0.2s;
+}
+
+.modal-close:hover {
+ color: #212529;
+}
+
+.modal-body {
+ padding: 0.75rem 1.25rem;
+}
+
+.form-group {
+ margin-bottom: 0.6rem;
+}
+
+.form-group label {
+ display: block;
+ font-weight: 600;
+ font-size: 0.8rem;
+ color: #495057;
+ margin-bottom: 0.25rem;
+}
+
+.form-group input[type="text"],
+.form-group textarea,
+.form-group select {
+ width: 100%;
+ padding: 0.4rem 0.75rem;
+ border: 1px solid #ced4da;
+ border-radius: 4px;
+ font-size: 0.85rem;
+ font-family: inherit;
+ transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+.form-group input[type="text"]:focus,
+.form-group textarea:focus,
+.form-group select:focus {
+ outline: none;
+ border-color: var(--main-color);
+ box-shadow: 0 0 0 0.2rem rgba(39, 79, 151, 0.15);
+}
+
+.form-group textarea {
+ min-height: 60px;
+ resize: vertical;
+}
+
+.form-group select[multiple] {
+ min-height: 60px;
+}
+
+.form-hint {
+ margin-top: 0.2rem;
+ font-size: 0.7rem;
+ color: #6c757d;
+}
+
+.modal-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.5rem;
+ padding-top: 0.5rem;
+ border-top: 1px solid #dee2e6;
+ margin-top: 0.15rem;
+}
+
+.btn-cancel {
+ background: transparent;
+ border: 1px solid #dee2e6;
+ padding: 0.4rem 1rem;
+ border-radius: 4px;
+ font-weight: 600;
+ font-size: 0.8rem;
+ cursor: pointer;
+ color: #495057;
+ transition: all 0.2s;
+}
+
+.btn-cancel:hover {
+ background: #f8f9fa;
+ border-color: #adb5bd;
+}
+
+.btn-submit {
+ background: var(--main-color);
+ border: none;
+ padding: 0.4rem 1rem;
+ border-radius: 4px;
+ font-weight: 600;
+ font-size: 0.8rem;
+ cursor: pointer;
+ color: white;
+ transition: background 0.2s;
+}
+
+.btn-submit:hover {
+ background: var(--secondary-color);
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .qa-container {
+ flex-direction: column;
+ }
+
+ .qa-sidebar {
+ flex: 0 0 auto;
+ max-height: 40vh;
+ }
+
+ .qa-content {
+ flex: 1;
+ }
+
+ .modal-container {
+ margin: 1rem;
+ max-height: calc(100vh - 2rem);
+ }
+}
+
+/* Course Search Results */
+.course-search-results {
+ display: none;
+ position: absolute;
+ z-index: 1050;
+ background: white;
+ border: 1px solid #ced4da;
+ border-top: none;
+ border-radius: 0 0 4px 4px;
+ max-height: 200px;
+ overflow-y: auto;
+ width: 100%;
+}
+
+.course-search-results.show {
+ display: block;
+}
+
+.course-result-item {
+ padding: 0.5rem 0.75rem;
+ cursor: pointer;
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.course-result-item:hover {
+ background: #f8f9fa;
+}
+
+.course-result-code {
+ font-weight: 600;
+ font-size: 0.875rem;
+}
+
+.course-result-title {
+ font-size: 0.8rem;
+ color: #6c757d;
+}
+
+/* Loading state for content panel */
+.qa-content.loading {
+ opacity: 0.5;
+ pointer-events: none;
+}
+
+/* No post selected / empty state */
+.no-post-selected,
+.no-posts {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ color: #6c757d;
+ text-align: center;
+ padding: 2rem;
+}
+
+.vote-btn.voted .fa-thumbs-up,
+.post-stats.voted .fa-thumbs-up {
+ background-image: url("../icons/img/fa-thumbs-up-solid.svg");
+ filter: brightness(0) saturate(100%) invert(51%) sepia(98%) saturate(1828%) hue-rotate(0deg) brightness(102%) contrast(103%);
+ transform: scale(1.2);
+}
+
+/* Login prompt */
+.login-prompt {
+ padding: 1rem 2rem;
+ color: #6c757d;
+}
+
+/* Form group relative for dropdown */
+.modal-body .form-group {
+ position: relative;
+}
+
+/* Required asterisk */
+.required {
+ color: red;
+}
+
+/* Dropdown overrides — replace Bootstrap gray defaults with brand styling */
+.qa-container .dropdown-menu {
+ border: 1px solid #dee2e6;
+ border-radius: 4px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ padding: 0.25rem 0;
+ background: #ffffff;
+ z-index: 30;
+}
+
+.qa-container .dropdown-item {
+ font-size: 0.875rem;
+ padding: 0.4rem 0.75rem;
+ color: #495057;
+ background: transparent;
+}
+
+.qa-container .dropdown-item:hover,
+.qa-container .dropdown-item:focus {
+ background: #f0f4ff;
+ color: var(--main-color);
+}
+
+.qa-container .dropdown-item.active,
+.qa-container .dropdown-item:active {
+ background: var(--main-color);
+ color: white;
+}
diff --git a/tcf_website/static/qa/qa_dashboard.js b/tcf_website/static/qa/qa_dashboard.js
new file mode 100644
index 000000000..c67bd4991
--- /dev/null
+++ b/tcf_website/static/qa/qa_dashboard.js
@@ -0,0 +1,1058 @@
+/**
+ * Q&A Dashboard JavaScript
+ * Handles all interactive functionality for the Q&A dashboard.
+ */
+
+document.addEventListener('DOMContentLoaded', function () {
+ initQuestionSelection();
+ initSearch();
+ initCourseFilter();
+ initNewPostModal();
+ initDeleteQuestionModal();
+ initDeleteAnswerModal();
+ initVoting();
+ initAnswerForm();
+ initQuestionActions();
+ initAnswerActions();
+ initReplyForms();
+ initTooltips();
+});
+
+function qaUrl(template, id) {
+ return template.replace('__ID__', String(id));
+}
+
+function showRequestError(message) {
+ window.alert(message);
+}
+
+// ─── Question Selection ───────────────────────────────────────────────────────
+
+function initQuestionSelection() {
+ document.querySelectorAll('.post-item').forEach(item => {
+ if (item.dataset.questionSelectionBound === 'true') return;
+ item.dataset.questionSelectionBound = 'true';
+
+ item.addEventListener('click', function () {
+ const questionId = this.dataset.questionId;
+ loadQuestionDetail(questionId);
+
+ document.querySelectorAll('.post-item').forEach(p => {
+ p.classList.remove('active');
+ p.setAttribute('aria-current', 'false');
+ });
+ this.classList.add('active');
+ this.setAttribute('aria-current', 'true');
+
+ const url = new URL(window.location);
+ url.searchParams.set('question', questionId);
+ window.history.pushState({}, '', url);
+ });
+ });
+}
+
+function loadQuestionDetail(questionId) {
+ const contentArea = document.getElementById('questionContent');
+ contentArea.classList.add('loading');
+
+ fetch(qaUrl(QA_URLS.questionDetail, questionId))
+ .then(r => r.text())
+ .then(html => {
+ contentArea.innerHTML = html;
+ contentArea.classList.remove('loading');
+ // Re-initialise handlers for newly injected HTML
+ initVoting();
+ initAnswerForm();
+ initQuestionActions();
+ initAnswerActions();
+ initReplyForms();
+ initTooltips();
+ })
+ .catch(() => {
+ contentArea.classList.remove('loading');
+ contentArea.innerHTML = '
Error loading question. Please try again.
';
+ });
+}
+
+function refreshDashboard(url) {
+ fetch(url.toString(), {
+ headers: { 'X-Requested-With': 'XMLHttpRequest' },
+ })
+ .then(r => r.json())
+ .then(data => {
+ const postsList = document.getElementById('postsList');
+ const contentArea = document.getElementById('questionContent');
+
+ if (postsList) postsList.innerHTML = data.posts_html;
+ if (contentArea) contentArea.innerHTML = data.detail_html;
+
+ initQuestionSelection();
+ initVoting();
+ initAnswerForm();
+ initQuestionActions();
+ initAnswerActions();
+ initReplyForms();
+ initTooltips();
+
+ if (data.selected_question_id) {
+ url.searchParams.set('question', data.selected_question_id);
+ }
+
+ window.history.pushState({}, '', url);
+ })
+ .catch(() => {
+ window.location.href = url.toString();
+ });
+}
+
+// ─── Search ───────────────────────────────────────────────────────────────────
+
+function initSearch() {
+ const input = document.getElementById('searchInput');
+ if (!input) return;
+
+ let timeout;
+ function runSearch() {
+ const q = input.value.trim();
+ const url = new URL(window.location);
+ if (q) url.searchParams.set('q', q);
+ else url.searchParams.delete('q');
+ url.searchParams.delete('question');
+ refreshDashboard(url);
+ }
+
+ input.addEventListener('input', function () {
+ clearTimeout(timeout);
+ timeout = setTimeout(() => {
+ runSearch();
+ }, 500);
+ });
+
+ input.addEventListener('keypress', function (e) {
+ if (e.key === 'Enter') {
+ clearTimeout(timeout);
+ e.preventDefault();
+ runSearch();
+ }
+ });
+}
+
+// ─── Course Filter ────────────────────────────────────────────────────────────
+
+function initCourseFilter() {
+ const departmentSearchInput = document.getElementById('departmentFilterSearch');
+ const departmentItemsList = document.getElementById('departmentFilterList');
+ const courseDropdownContainer = document.getElementById('courseDropdownContainer');
+ const courseSearchInput = document.getElementById('courseFilterSearch');
+ const courseItemsList = document.getElementById('courseFilterList');
+ const departmentLabel = document.getElementById('departmentLabel');
+ const courseLabel = document.getElementById('courseLabel');
+ const departmentDropdownBtn = document.getElementById('departmentDropdown');
+ const courseDropdownBtn = document.getElementById('courseDropdown');
+
+ if (!departmentSearchInput || !departmentItemsList || !courseDropdownContainer || !courseSearchInput || !courseItemsList) return;
+
+ // Get currently selected department from the page
+ let selectedDepartmentId = '';
+ const activeDeptItem = departmentItemsList.querySelector('.dropdown-item.active');
+ if (activeDeptItem && activeDeptItem.dataset.departmentId) {
+ selectedDepartmentId = activeDeptItem.dataset.departmentId;
+ }
+
+ let selectedCourseId = '';
+ let selectedScope = '';
+ const activeCourseItem = courseItemsList.querySelector('.dropdown-item.active');
+ if (activeCourseItem && activeCourseItem.dataset.courseId) {
+ selectedCourseId = activeCourseItem.dataset.courseId;
+ }
+ if (activeCourseItem && activeCourseItem.dataset.scope) {
+ selectedScope = activeCourseItem.dataset.scope;
+ }
+
+ // Show course dropdown if a department is selected
+ if (selectedDepartmentId) {
+ setCourseDropdownVisible(true);
+ filterCoursesByDepartment(selectedDepartmentId);
+ } else {
+ setCourseDropdownVisible(false);
+ }
+
+ updateFilterButtonStates();
+
+ // ─── Department Dropdown Handling ───
+ if (departmentDropdownBtn) {
+ $(departmentDropdownBtn).closest('.dropdown').on('shown.bs.dropdown', function () {
+ departmentSearchInput.value = '';
+ departmentSearchInput.focus();
+ departmentItemsList.querySelectorAll('.dropdown-item').forEach(item => {
+ item.classList.remove('d-none');
+ });
+ });
+ }
+
+ departmentSearchInput.addEventListener('click', function (e) {
+ e.stopPropagation();
+ });
+
+ departmentSearchInput.addEventListener('input', function () {
+ const query = this.value.trim().toLowerCase();
+ departmentItemsList.querySelectorAll('.dropdown-item').forEach(item => {
+ const text = item.textContent.trim().toLowerCase();
+ if (!query || text === 'all departments' || text.includes(query)) {
+ item.classList.remove('d-none');
+ } else {
+ item.classList.add('d-none');
+ }
+ });
+ });
+
+ departmentItemsList.querySelectorAll('.dropdown-item').forEach(item => {
+ item.addEventListener('click', function (e) {
+ const deptId = this.dataset.departmentId;
+ const deptText = this.textContent.trim();
+
+ // Update department label
+ if (departmentLabel) {
+ departmentLabel.textContent = deptText === 'All Departments' ? 'All Departments' : deptText;
+ }
+
+ // Update active state
+ departmentItemsList.querySelectorAll('.dropdown-item').forEach(i => i.classList.remove('active'));
+ this.classList.add('active');
+
+ // Reset course dropdown and show/hide based on selection
+ if (!deptId) {
+ selectedDepartmentId = '';
+ selectedCourseId = '';
+ selectedScope = '';
+ // All Departments selected
+ setCourseDropdownVisible(false);
+ if (courseLabel) courseLabel.textContent = 'All Posts';
+ const url = new URL(window.location);
+ url.searchParams.delete('department');
+ url.searchParams.delete('course');
+ url.searchParams.delete('scope');
+ url.searchParams.delete('question');
+ updateFilterButtonStates();
+ refreshDashboard(url);
+ } else {
+ selectedDepartmentId = deptId;
+ selectedCourseId = '';
+ selectedScope = '';
+ // Specific department selected - show course dropdown
+ setCourseDropdownVisible(true);
+ if (courseLabel) courseLabel.textContent = 'All Posts';
+
+ // Filter and show courses for this department
+ filterCoursesByDepartment(deptId);
+
+ const url = new URL(window.location);
+ url.searchParams.set('department', deptId);
+ url.searchParams.delete('course');
+ url.searchParams.delete('scope');
+ url.searchParams.delete('question');
+ updateFilterButtonStates();
+ refreshDashboard(url);
+ }
+
+ e.preventDefault();
+ });
+ });
+
+ // ─── Course Dropdown Handling ───
+ if (courseDropdownBtn) {
+ $(courseDropdownBtn).closest('.dropdown').on('shown.bs.dropdown', function () {
+ courseSearchInput.value = '';
+ courseSearchInput.focus();
+
+ // Reapply current department filter whenever the menu opens.
+ filterCoursesByDepartment(selectedDepartmentId);
+ });
+ }
+
+ courseSearchInput.addEventListener('click', function (e) {
+ e.stopPropagation();
+ });
+
+ courseSearchInput.addEventListener('input', function () {
+ const query = this.value.trim().toLowerCase();
+ filterCoursesByDepartment(selectedDepartmentId, query);
+ });
+
+ courseItemsList.querySelectorAll('.dropdown-item').forEach(item => {
+ item.addEventListener('click', function (e) {
+ const courseId = this.dataset.courseId;
+ const scope = this.dataset.scope || '';
+ const courseText = this.textContent.trim();
+
+ // Update course label
+ if (courseLabel) {
+ courseLabel.textContent = courseText === 'All Posts' ? 'All Posts' : courseText;
+ }
+
+ // Navigate to the selected course
+ if (courseId) {
+ selectedCourseId = courseId;
+ selectedScope = '';
+ const url = new URL(window.location);
+ if (selectedDepartmentId) {
+ url.searchParams.set('department', selectedDepartmentId);
+ }
+ url.searchParams.set('course', courseId);
+ url.searchParams.delete('scope');
+ url.searchParams.delete('question');
+ updateFilterButtonStates();
+ refreshDashboard(url);
+ } else if (scope === 'department_broad') {
+ selectedCourseId = '';
+ selectedScope = 'department_broad';
+ const url = new URL(window.location);
+ if (selectedDepartmentId) {
+ url.searchParams.set('department', selectedDepartmentId);
+ }
+ url.searchParams.delete('course');
+ url.searchParams.set('scope', 'department_broad');
+ url.searchParams.delete('question');
+ updateFilterButtonStates();
+ refreshDashboard(url);
+ } else {
+ selectedCourseId = '';
+ selectedScope = '';
+ // All Posts selected
+ const activeDeptItem = departmentItemsList.querySelector('.dropdown-item.active');
+ const deptId = activeDeptItem ? activeDeptItem.dataset.departmentId : '';
+ if (deptId) {
+ // Stay in the department but show all courses
+ const url = new URL(window.location);
+ url.searchParams.set('department', deptId);
+ url.searchParams.delete('course');
+ url.searchParams.delete('scope');
+ url.searchParams.delete('question');
+ updateFilterButtonStates();
+ refreshDashboard(url);
+ } else {
+ // Shouldn't happen, but go back to all
+ const url = new URL(window.location);
+ url.searchParams.delete('department');
+ url.searchParams.delete('course');
+ url.searchParams.delete('scope');
+ url.searchParams.delete('question');
+ updateFilterButtonStates();
+ refreshDashboard(url);
+ }
+ }
+
+ e.preventDefault();
+ });
+ });
+
+ function filterCoursesByDepartment(departmentId, searchQuery = '') {
+ const normalizedQuery = searchQuery.trim().toLowerCase();
+ courseItemsList.querySelectorAll('.dropdown-item').forEach(item => {
+ if (!item.dataset.courseId) {
+ // "All Courses" option
+ item.classList.remove('d-none');
+ } else {
+ const sameDepartment = !departmentId || item.dataset.departmentId === departmentId;
+ const text = item.textContent.trim().toLowerCase();
+ const matchesQuery = !normalizedQuery || text.includes(normalizedQuery);
+
+ if (sameDepartment && matchesQuery) {
+ item.classList.remove('d-none');
+ } else {
+ item.classList.add('d-none');
+ }
+ }
+ });
+ if (!searchQuery) {
+ courseSearchInput.value = '';
+ }
+ }
+
+ function setCourseDropdownVisible(isVisible) {
+ if (isVisible) {
+ courseDropdownContainer.style.display = 'block';
+ requestAnimationFrame(() => {
+ courseDropdownContainer.classList.add('is-visible');
+ });
+ } else {
+ courseDropdownContainer.classList.remove('is-visible');
+ window.setTimeout(() => {
+ if (!courseDropdownContainer.classList.contains('is-visible')) {
+ courseDropdownContainer.style.display = 'none';
+ }
+ }, 200);
+ }
+ }
+
+ function updateFilterButtonStates() {
+ if (departmentDropdownBtn) {
+ departmentDropdownBtn.classList.toggle('filter-active', Boolean(selectedDepartmentId));
+ }
+ if (courseDropdownBtn) {
+ courseDropdownBtn.classList.toggle(
+ 'filter-active',
+ Boolean(selectedCourseId || selectedScope)
+ );
+ }
+ }
+}
+
+// ─── New Post Modal ───────────────────────────────────────────────────────────
+
+function initNewPostModal() {
+ const modal = document.getElementById('newPostModal');
+ if (!modal) return;
+
+ const openBtn = document.getElementById('openNewPostModal');
+ const closeBtn = document.getElementById('closeModal');
+ const cancelBtn = document.getElementById('cancelModal');
+ const form = document.getElementById('newPostForm');
+ const courseSearchInput = document.getElementById('courseSearch');
+ const courseIdInput = document.getElementById('courseId');
+ const departmentIdInput = document.getElementById('departmentId');
+ const courseResults = document.getElementById('courseResults');
+ const instructorSearchInput = document.getElementById('instructorSearch');
+ const instructorIdInput = document.getElementById('instructorId');
+ const instructorResults = document.getElementById('instructorResults');
+ let instructorOptions = [];
+ let selectedTargetType = 'none';
+ let instructorRequestToken = 0;
+
+ function escapeHtml(value) {
+ return String(value)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ }
+
+ function resetInstructorSearch() {
+ instructorOptions = [];
+ if (instructorIdInput) instructorIdInput.value = '';
+ if (instructorSearchInput) {
+ instructorSearchInput.value = '';
+ instructorSearchInput.disabled = true;
+ instructorSearchInput.placeholder = 'Select a course first...';
+ }
+ if (instructorResults) {
+ instructorResults.classList.remove('show');
+ instructorResults.innerHTML = '';
+ }
+ }
+
+ function setDepartmentMode() {
+ selectedTargetType = 'department';
+ // Invalidate any in-flight instructor request from previous course selection.
+ instructorRequestToken += 1;
+ if (instructorIdInput) instructorIdInput.value = '';
+ if (instructorSearchInput) {
+ instructorSearchInput.value = '';
+ instructorSearchInput.disabled = true;
+ instructorSearchInput.placeholder = 'Instructor not required for department posts';
+ }
+ if (instructorResults) {
+ instructorResults.classList.remove('show');
+ instructorResults.innerHTML = '';
+ }
+ }
+
+ function renderInstructorResults(query = '') {
+ if (!instructorResults || !instructorSearchInput) return;
+
+ const normalizedQuery = query.trim().toLowerCase();
+ const filteredOptions = instructorOptions.filter(instructor =>
+ instructor.name.toLowerCase().includes(normalizedQuery)
+ );
+
+ if (filteredOptions.length === 0) {
+ instructorResults.innerHTML = 'No instructors found
';
+ instructorResults.classList.add('show');
+ return;
+ }
+
+ instructorResults.innerHTML = filteredOptions.map(instructor =>
+ `
+
${escapeHtml(instructor.name)}
+
`
+ ).join('');
+ instructorResults.classList.add('show');
+
+ instructorResults.querySelectorAll('.course-result-item').forEach(item => {
+ item.addEventListener('click', function () {
+ if (instructorIdInput) instructorIdInput.value = this.dataset.id;
+ instructorSearchInput.value = this.dataset.name;
+ instructorResults.classList.remove('show');
+ });
+ });
+ }
+
+ function openModal() {
+ modal.style.display = 'flex';
+ document.body.style.overflow = 'hidden';
+ }
+
+ function closeModal() {
+ modal.style.display = 'none';
+ document.body.style.overflow = '';
+ if (form) form.reset();
+ if (courseIdInput) courseIdInput.value = '';
+ if (departmentIdInput) departmentIdInput.value = '';
+ selectedTargetType = 'none';
+ instructorRequestToken += 1;
+ if (courseResults) courseResults.classList.remove('show');
+ resetInstructorSearch();
+ }
+
+ if (openBtn) openBtn.addEventListener('click', openModal);
+ if (closeBtn) closeBtn.addEventListener('click', closeModal);
+ if (cancelBtn) cancelBtn.addEventListener('click', closeModal);
+ modal.addEventListener('click', e => { if (e.target === modal) closeModal(); });
+
+ // Course search autocomplete
+ if (courseSearchInput) {
+ let searchTimeout;
+ courseSearchInput.addEventListener('input', function () {
+ if (courseIdInput) courseIdInput.value = '';
+ if (departmentIdInput) departmentIdInput.value = '';
+ selectedTargetType = 'none';
+ instructorRequestToken += 1;
+ resetInstructorSearch();
+ clearTimeout(searchTimeout);
+ const q = this.value.trim();
+ if (q.length < 2) {
+ courseResults.classList.remove('show');
+ return;
+ }
+ searchTimeout = setTimeout(() => {
+ fetch(`${QA_URLS.searchCourses}?q=${encodeURIComponent(q)}`)
+ .then(r => r.json())
+ .then(data => {
+ if (data.results && data.results.length > 0) {
+ courseResults.innerHTML = data.results.map(c => {
+ const id = escapeHtml(c.id);
+ const code = escapeHtml(c.code);
+ const type = escapeHtml(c.type);
+ const title = escapeHtml(c.title);
+ return ``
+ }).join('');
+ courseResults.classList.add('show');
+
+ courseResults.querySelectorAll('.course-result-item').forEach(item => {
+ item.addEventListener('click', function () {
+ courseSearchInput.value = this.dataset.code;
+ courseResults.classList.remove('show');
+ if (this.dataset.type === 'course') {
+ selectedTargetType = 'course';
+ courseIdInput.value = this.dataset.id;
+ if (departmentIdInput) departmentIdInput.value = '';
+ resetInstructorSearch();
+ const requestToken = ++instructorRequestToken;
+ loadInstructors(this.dataset.id, requestToken);
+ } else {
+ if (courseIdInput) courseIdInput.value = '';
+ if (departmentIdInput) departmentIdInput.value = this.dataset.id;
+ setDepartmentMode();
+ }
+ });
+ });
+ } else {
+ courseResults.innerHTML = 'No courses found
';
+ courseResults.classList.add('show');
+ }
+ });
+ }, 300);
+ });
+
+ courseSearchInput.addEventListener('keydown', function (e) {
+ if (e.key === 'Backspace' && courseIdInput.value) {
+ courseIdInput.value = '';
+ selectedTargetType = 'none';
+ instructorRequestToken += 1;
+ resetInstructorSearch();
+ }
+ if (e.key === 'Backspace' && departmentIdInput && departmentIdInput.value) {
+ departmentIdInput.value = '';
+ selectedTargetType = 'none';
+ instructorRequestToken += 1;
+ resetInstructorSearch();
+ }
+ });
+
+ document.addEventListener('click', function (e) {
+ if (!courseSearchInput.contains(e.target) && !courseResults.contains(e.target)) {
+ courseResults.classList.remove('show');
+ }
+ });
+ }
+
+ if (instructorSearchInput && instructorResults) {
+ instructorSearchInput.addEventListener('click', function (e) {
+ e.stopPropagation();
+ if (!this.disabled) {
+ renderInstructorResults(this.value);
+ }
+ });
+
+ instructorSearchInput.addEventListener('input', function () {
+ if (instructorIdInput) instructorIdInput.value = '';
+ if (this.disabled) return;
+ renderInstructorResults(this.value);
+ });
+
+ document.addEventListener('click', function (e) {
+ if (!instructorSearchInput.contains(e.target) && !instructorResults.contains(e.target)) {
+ instructorResults.classList.remove('show');
+ }
+ });
+ }
+
+ function loadInstructors(courseId, requestToken) {
+ if (!instructorSearchInput || !instructorResults) return;
+ instructorSearchInput.disabled = true;
+ instructorSearchInput.placeholder = 'Loading instructors...';
+ instructorSearchInput.value = '';
+ instructorResults.classList.remove('show');
+ instructorResults.innerHTML = 'Loading instructors...
';
+ instructorResults.classList.add('show');
+
+ fetch(qaUrl(QA_URLS.getInstructors, courseId))
+ .then(r => r.json())
+ .then(data => {
+ if (requestToken !== instructorRequestToken || selectedTargetType !== 'course') {
+ return;
+ }
+ instructorOptions = data.instructors || [];
+ instructorSearchInput.disabled = false;
+ instructorSearchInput.placeholder = instructorOptions.length > 0 ? 'Search instructors...' : 'No instructors found';
+ instructorSearchInput.focus();
+
+ if (instructorOptions.length > 0) {
+ renderInstructorResults('');
+ } else {
+ instructorResults.innerHTML = 'No instructors found
';
+ instructorResults.classList.add('show');
+ }
+ })
+ .catch(() => {
+ if (requestToken !== instructorRequestToken || selectedTargetType !== 'course') {
+ return;
+ }
+ instructorOptions = [];
+ instructorSearchInput.disabled = false;
+ instructorSearchInput.placeholder = 'Error loading instructors';
+ instructorResults.innerHTML = 'Error loading instructors
';
+ instructorResults.classList.add('show');
+ });
+ }
+
+ if (form) {
+ form.addEventListener('submit', function (e) {
+ const hasCourse = courseIdInput && courseIdInput.value;
+ const hasDepartment = departmentIdInput && departmentIdInput.value;
+
+ if (!hasCourse && !hasDepartment) {
+ e.preventDefault();
+ courseSearchInput.focus();
+ courseSearchInput.setCustomValidity('Please select a course or a department.');
+ courseSearchInput.reportValidity();
+ return;
+ }
+
+ courseSearchInput.setCustomValidity('');
+
+ if (hasDepartment && instructorIdInput) {
+ instructorIdInput.value = '';
+ }
+ if (hasDepartment && courseIdInput) {
+ courseIdInput.value = '';
+ }
+ });
+ }
+}
+
+// ─── Delete Question Modal ────────────────────────────────────────────────────
+
+let questionIdToDelete = null;
+
+function initDeleteQuestionModal() {
+ const confirmDeleteBtn = document.getElementById('confirmDeleteQuestion');
+
+ if (confirmDeleteBtn) {
+ confirmDeleteBtn.addEventListener('click', function () {
+ if (!questionIdToDelete) return;
+
+ const deleteData = new FormData();
+ deleteData.append('csrfmiddlewaretoken', CSRF_TOKEN);
+ fetch(qaUrl(QA_URLS.deleteQuestion, questionIdToDelete), {
+ method: 'POST',
+ headers: {
+ 'X-CSRFToken': CSRF_TOKEN,
+ 'X-Requested-With': 'XMLHttpRequest',
+ },
+ body: deleteData,
+ })
+ .then(r => {
+ if (!r.ok) throw new Error(r.status);
+ return r;
+ })
+ .then(() => {
+ $('#deleteQuestionModal').modal('hide');
+ window.location.href = QA_URLS.dashboard;
+ })
+ .catch(err => {
+ console.error('Delete question error:', err);
+ showRequestError('Unable to delete the question right now. Please try again.');
+ });
+ });
+ }
+}
+
+function openDeleteQuestionModal(qId) {
+ questionIdToDelete = qId;
+ $('#deleteQuestionModal').modal('show');
+}
+
+// ─── Delete Answer Modal ──────────────────────────────────────────────────────
+
+let answerIdToDelete = null;
+
+function initDeleteAnswerModal() {
+ const confirmDeleteBtn = document.getElementById('confirmDeleteAnswer');
+
+ if (confirmDeleteBtn) {
+ confirmDeleteBtn.addEventListener('click', function () {
+ if (!answerIdToDelete) return;
+
+ const deleteData = new FormData();
+ deleteData.append('csrfmiddlewaretoken', CSRF_TOKEN);
+ fetch(qaUrl(QA_URLS.deleteAnswer, answerIdToDelete), {
+ method: 'POST',
+ headers: {
+ 'X-CSRFToken': CSRF_TOKEN,
+ 'X-Requested-With': 'XMLHttpRequest',
+ },
+ body: deleteData,
+ })
+ .then(r => {
+ if (!r.ok) throw new Error(r.status);
+ return r;
+ })
+ .then(() => {
+ $('#deleteAnswerModal').modal('hide');
+ const url = new URL(window.location);
+ const qId = url.searchParams.get('question');
+ const activeItem = document.querySelector('.post-item.active');
+ const questionId = qId || (activeItem && activeItem.dataset.questionId);
+ if (questionId) loadQuestionDetail(questionId);
+ })
+ .catch(err => {
+ console.error('Delete answer error:', err);
+ showRequestError('Unable to delete the answer right now. Please try again.');
+ });
+ });
+ }
+}
+
+function openDeleteAnswerModal(aId) {
+ answerIdToDelete = aId;
+ $('#deleteAnswerModal').modal('show');
+}
+
+// ─── Voting ───────────────────────────────────────────────────────────────────
+
+function initVoting() {
+ document.querySelectorAll('.vote-btn').forEach(btn => {
+ if (btn.dataset.votingBound === 'true') return;
+ btn.dataset.votingBound = 'true';
+
+ btn.addEventListener('click', function () {
+ if (!IS_AUTHENTICATED) {
+ window.location.href = '/login/';
+ return;
+ }
+
+ const type = this.dataset.type; // 'question' or 'answer'
+ const id = this.dataset.id;
+
+ const url = type === 'question'
+ ? qaUrl(QA_URLS.upvoteQuestion, id)
+ : qaUrl(QA_URLS.upvoteAnswer, id);
+
+ const counterId = type === 'question'
+ ? `question-vote-count-${id}`
+ : `answer-vote-count-${id}`;
+
+ fetch(url, {
+ method: 'POST',
+ headers: { 'X-CSRFToken': CSRF_TOKEN },
+ })
+ .then(r => {
+ if (!r.ok) throw new Error(r.status);
+ return r.json();
+ })
+ .then(data => {
+ if (data.ok) {
+ const container = this.closest('.post-actions');
+ const upBtn = container
+ ? container.querySelector('[data-action="up"]')
+ : this;
+ const counterEl = document.getElementById(counterId);
+
+ if (upBtn) upBtn.classList.toggle('voted', data.user_vote === 1);
+ if (counterEl) counterEl.textContent = data.votes;
+
+ // Keep sidebar list in sync for question votes
+ if (type === 'question') {
+ const sidebarContainer = document.querySelector(`#sidebar-vote-count-${id}`);
+ if (sidebarContainer) {
+ const sidebarNum = sidebarContainer.querySelector('.sidebar-vote-num');
+ if(sidebarNum) sidebarNum.textContent = data.votes;
+ sidebarContainer.classList.toggle('voted', data.user_vote === 1)
+ }
+ }
+ }
+ })
+ .catch(err => {
+ console.error('Vote error:', err);
+ showRequestError('Unable to record your vote right now. Please try again.');
+ });
+ });
+ });
+}
+
+// ─── Answer Form ──────────────────────────────────────────────────────────────
+
+function initAnswerForm() {
+ const form = document.getElementById('mainAnswerForm');
+ if (!form) return;
+
+ const newForm = form.cloneNode(true);
+ form.parentNode.replaceChild(newForm, form);
+
+ newForm.addEventListener('submit', function (e) {
+ e.preventDefault();
+
+ const formData = new FormData(newForm);
+ const submitBtn = newForm.querySelector('.btn-submit-reply');
+
+ if (submitBtn) {
+ submitBtn.disabled = true;
+ submitBtn.textContent = 'Posting...';
+ }
+
+ fetch(newForm.action, {
+ method: 'POST',
+ body: formData,
+ headers: { 'X-Requested-With': 'XMLHttpRequest' },
+ })
+ .then(r => {
+ if (!r.ok) throw new Error(r.status);
+ return r;
+ })
+ .then(() => {
+ incrementAnswerCount();
+ // Reload the current question detail
+ const url = new URL(window.location);
+ const questionId = url.searchParams.get('question');
+ const activeItem = document.querySelector('.post-item.active');
+ const qId = questionId || (activeItem && activeItem.dataset.questionId);
+ if (qId) loadQuestionDetail(qId);
+ })
+ .catch(err => {
+ console.error('Answer submit error:', err);
+ showRequestError('Unable to post your answer right now. Please try again.');
+ })
+ .finally(() => {
+ if (submitBtn) {
+ submitBtn.disabled = false;
+ submitBtn.textContent = 'Post Answer';
+ }
+ });
+ });
+}
+
+function incrementAnswerCount() {
+ const counter = document.querySelector('.response-count');
+ if (!counter) return;
+
+ const match = counter.textContent.match(/\d+/);
+ if (!match) return;
+
+ counter.textContent = `(${parseInt(match[0], 10) + 1})`;
+}
+
+function initTooltips() {
+ if (typeof $ !== 'function') return;
+ $('[data-toggle="tooltip"]').tooltip();
+}
+
+// ─── Question Actions (Edit / Delete) ────────────────────────────────────────
+
+function initQuestionActions() {
+ const editModal = document.getElementById('editQuestionModal');
+
+ document.querySelectorAll('.edit-question-btn').forEach(btn => {
+ if (btn.dataset.questionActionBound === 'true') return;
+ btn.dataset.questionActionBound = 'true';
+ btn.addEventListener('click', function (e) {
+ e.preventDefault();
+ if (!editModal) return;
+
+ const qId = this.dataset.questionId;
+ document.getElementById('editTitle').value = this.dataset.title || '';
+ document.getElementById('editText').value = this.dataset.text || '';
+ document.getElementById('editCourse').value = this.dataset.course || '';
+ document.getElementById('editDepartment').value = this.dataset.department || '';
+ document.getElementById('editInstructor').value = this.dataset.instructor || '';
+ document.getElementById('editQuestionForm').action = qaUrl(QA_URLS.editQuestion, qId);
+
+ editModal.style.display = 'flex';
+ document.body.style.overflow = 'hidden';
+ });
+ });
+
+ const closeEditBtn = document.getElementById('closeEditModal');
+ const cancelEditBtn = document.getElementById('cancelEditModal');
+ function closeEditModal() {
+ if (editModal) {
+ editModal.style.display = 'none';
+ document.body.style.overflow = '';
+ }
+ }
+ if (closeEditBtn && closeEditBtn.dataset.modalBound !== 'true') {
+ closeEditBtn.dataset.modalBound = 'true';
+ closeEditBtn.addEventListener('click', closeEditModal);
+ }
+ if (cancelEditBtn && cancelEditBtn.dataset.modalBound !== 'true') {
+ cancelEditBtn.dataset.modalBound = 'true';
+ cancelEditBtn.addEventListener('click', closeEditModal);
+ }
+ if (editModal && editModal.dataset.modalBound !== 'true') {
+ editModal.dataset.modalBound = 'true';
+ editModal.addEventListener('click', e => { if (e.target === editModal) closeEditModal(); });
+ }
+
+ // Delete question button handler
+ document.querySelectorAll('.delete-question-btn').forEach(btn => {
+ if (btn.dataset.deleteQuestionBound === 'true') return;
+ btn.dataset.deleteQuestionBound = 'true';
+ btn.addEventListener('click', function (e) {
+ e.preventDefault();
+ const qId = this.dataset.questionId;
+ openDeleteQuestionModal(qId);
+ });
+ });
+}
+
+// ─── Answer Actions (Edit / Delete) ──────────────────────────────────────────
+
+function initAnswerActions() {
+ const editAnswerModal = document.getElementById('editAnswerModal');
+
+ document.querySelectorAll('.edit-answer-btn').forEach(btn => {
+ if (btn.dataset.answerActionBound === 'true') return;
+ btn.dataset.answerActionBound = 'true';
+ btn.addEventListener('click', function (e) {
+ e.preventDefault();
+ if (!editAnswerModal) return;
+
+ const answerId = this.dataset.answerId;
+ document.getElementById('editAnswerText').value = this.dataset.text || '';
+ const semSelect = document.getElementById('editAnswerSemester');
+ if (semSelect) semSelect.value = this.dataset.semester || '';
+ const mainForm = document.getElementById('mainAnswerForm');
+ const questionId = mainForm ? mainForm.querySelector('[name="question"]').value : '';
+ document.getElementById('editAnswerQuestion').value = questionId;
+ document.getElementById('editAnswerForm').action = qaUrl(QA_URLS.editAnswer, answerId);
+
+ editAnswerModal.style.display = 'flex';
+ document.body.style.overflow = 'hidden';
+ });
+ });
+
+ function closeEditAnswerModal() {
+ if (editAnswerModal) {
+ editAnswerModal.style.display = 'none';
+ document.body.style.overflow = '';
+ }
+ }
+ const closeBtn = document.getElementById('closeEditAnswerModal');
+ const cancelBtn = document.getElementById('cancelEditAnswerModal');
+ if (closeBtn && closeBtn.dataset.modalBound !== 'true') {
+ closeBtn.dataset.modalBound = 'true';
+ closeBtn.addEventListener('click', closeEditAnswerModal);
+ }
+ if (cancelBtn && cancelBtn.dataset.modalBound !== 'true') {
+ cancelBtn.dataset.modalBound = 'true';
+ cancelBtn.addEventListener('click', closeEditAnswerModal);
+ }
+ if (editAnswerModal && editAnswerModal.dataset.modalBound !== 'true') {
+ editAnswerModal.dataset.modalBound = 'true';
+ editAnswerModal.addEventListener('click', e => {
+ if (e.target === editAnswerModal) closeEditAnswerModal();
+ });
+ }
+
+ document.querySelectorAll('.delete-answer-btn').forEach(btn => {
+ if (btn.dataset.deleteAnswerBound === 'true') return;
+ btn.dataset.deleteAnswerBound = 'true';
+ btn.addEventListener('click', function (e) {
+ e.preventDefault();
+ const answerId = this.dataset.answerId;
+ openDeleteAnswerModal(answerId);
+ });
+ });
+}
+
+// ─── Reply Forms ──────────────────────────────────────────────────────────────
+
+function initReplyForms() {
+ // Handle Reply button clicks
+ document.querySelectorAll('.reply-btn').forEach(btn => {
+ if (btn.dataset.replyBound === 'true') return;
+ btn.dataset.replyBound = 'true';
+
+ btn.addEventListener('click', function (e) {
+ e.preventDefault();
+ const answerId = this.dataset.answerId;
+ const replyForm = document.getElementById(`reply-form-${answerId}`);
+
+ // Hide all other reply forms
+ document.querySelectorAll('.reply-form-container').forEach(form => {
+ if (form.id !== `reply-form-${answerId}`) {
+ form.style.display = 'none';
+ }
+ });
+
+ // Toggle this reply form
+ if (replyForm) {
+ replyForm.style.display = replyForm.style.display === 'none' ? 'block' : 'none';
+ }
+ });
+ });
+
+ // Handle Cancel button clicks
+ document.querySelectorAll('.btn-cancel-reply').forEach(btn => {
+ if (btn.dataset.replyCancelBound === 'true') return;
+ btn.dataset.replyCancelBound = 'true';
+
+ btn.addEventListener('click', function (e) {
+ e.preventDefault();
+ const replyForm = this.closest('.reply-form-container');
+ if (replyForm) {
+ replyForm.style.display = 'none';
+ // Clear the form
+ const form = replyForm.querySelector('form');
+ if (form) {
+ form.querySelector('textarea').value = '';
+ form.querySelector('select').selectedIndex = 0;
+ }
+ }
+ });
+ });
+}
diff --git a/tcf_website/templates/base/sidebar.html b/tcf_website/templates/base/sidebar.html
index 5a50bd131..4d941670b 100644
--- a/tcf_website/templates/base/sidebar.html
+++ b/tcf_website/templates/base/sidebar.html
@@ -38,6 +38,33 @@
{% endif %}
+
+
+ {% if user.is_authenticated %}
+
+ New
+
+
+
+ Q&A
+
+
+ {% else %}
+
+ New
+
+
+
+ Q&A
+
+
+ {% endif %}
+
+
{% if user.is_authenticated %}
diff --git a/tcf_website/templates/qa/_answer_item.html b/tcf_website/templates/qa/_answer_item.html
new file mode 100644
index 000000000..66bb8c7bc
--- /dev/null
+++ b/tcf_website/templates/qa/_answer_item.html
@@ -0,0 +1,101 @@
+
+
+
+
+ {% if answer.is_deleted_placeholder %}
+
[deleted]
+ {% else %}
+ {{ answer.text|linebreaks }}
+ {% endif %}
+
+
+ {% if answer.is_deleted_placeholder %}
+
+ {{ answer.sum_a_votes|default:0 }}
+
+ {% if user.is_authenticated %}
+
+ Reply
+
+ {% endif %}
+ {% elif user.is_authenticated %}
+
+
+ {{ answer.sum_a_votes|default:0 }}
+
+
+ Reply
+
+ {% else %}
+
+ {{ answer.sum_a_votes|default:0 }}
+
+ {% endif %}
+
+
+
+ {% if user.is_authenticated %}
+
+ {% endif %}
+
+
+{% if answer.nested_replies %}
+
+ {% for reply in answer.nested_replies %}
+ {% include 'qa/_answer_item.html' with answer=reply question=question semesters=semesters %}
+ {% endfor %}
+
+{% endif %}
diff --git a/tcf_website/templates/qa/_question_content.html b/tcf_website/templates/qa/_question_content.html
new file mode 100644
index 000000000..7808dc1f8
--- /dev/null
+++ b/tcf_website/templates/qa/_question_content.html
@@ -0,0 +1,8 @@
+{% if selected_question %}
+{% include 'qa/_question_detail.html' with question=selected_question answers=answers semesters=semesters %}
+{% else %}
+
+
+
Select a question to view its details
+
+{% endif %}
diff --git a/tcf_website/templates/qa/_question_detail.html b/tcf_website/templates/qa/_question_detail.html
new file mode 100644
index 000000000..884942834
--- /dev/null
+++ b/tcf_website/templates/qa/_question_detail.html
@@ -0,0 +1,164 @@
+{% load static %}
+
+
+
+
+ {% if question.course %}
+
+ {% elif question.department %}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+ {% if question.is_deleted %}
+
[deleted]
+ {% else %}
+ {{ question.text|linebreaks }}
+ {% endif %}
+
+
+
+
+ {% if question.is_deleted %}
+
+ {{ question.sum_q_votes|default:0 }}
+
+ {% elif user.is_authenticated %}
+
+
+ {{ question.sum_q_votes|default:0 }}
+
+
+ {% if user == question.user %}
+
+
+ Edit
+
+
+ Delete
+
+ {% endif %}
+ {% else %}
+
+ {{ question.sum_q_votes|default:0 }}
+
+ {% endif %}
+
+
+
+
+
+
+
+ {% if user.is_authenticated %}
+
+ {% else %}
+
+
Login to post an answer.
+
+ {% endif %}
+
diff --git a/tcf_website/templates/qa/_question_list.html b/tcf_website/templates/qa/_question_list.html
new file mode 100644
index 000000000..264397b30
--- /dev/null
+++ b/tcf_website/templates/qa/_question_list.html
@@ -0,0 +1,43 @@
+{% for question in questions %}
+
+
+
+ {% if question.is_deleted %}
+ [deleted]
+ {% else %}
+ {{ question.title|default:question.text|truncatechars:60 }}
+ {% endif %}
+
+
+ {% if question.is_deleted %}
+ [deleted]
+ {% else %}
+ {{ question.text|truncatechars:80 }}
+ {% endif %}
+
+
+
+{% empty %}
+
+
+
No questions found.
+
+{% endfor %}
diff --git a/tcf_website/templates/qa/qa.html b/tcf_website/templates/qa/qa.html
index 43b782c7b..baad78d21 100644
--- a/tcf_website/templates/qa/qa.html
+++ b/tcf_website/templates/qa/qa.html
@@ -104,12 +104,18 @@ Updated {{ question.created|date:"n/d/y" }}
-
{{ question.text|linebreaks }}
+
+ {% if question.is_deleted %}
+ [deleted]
+ {% else %}
+ {{ question.text|linebreaks }}
+ {% endif %}
+
+ Answers + ({{ answer_count }}) +
+ +{% endif %} + {% empty %} +
No answers yet. Be the first to answer!
+