diff --git a/src/accounts/urls.py b/src/accounts/urls.py
index edd9c7c..bff30ea 100644
--- a/src/accounts/urls.py
+++ b/src/accounts/urls.py
@@ -3,7 +3,7 @@
from django.contrib.auth import views as auth_views
from django.urls import path, include
-from .views import UserLoginView, ServiceProviderLoginView, register
+from .views import UserLoginView, ServiceProviderLoginView, register, CustomLogoutView
from accounts import views
@@ -17,12 +17,8 @@
ServiceProviderLoginView.as_view(),
name="service_provider_login",
),
- path(
- "profile/", views.profile_view, name="profile_view"
- ), # TODO: what happen when logged in as service provider.
- path(
- "logout/", auth_views.LogoutView.as_view(next_page="user_login"), name="logout"
- ), # Logout URL
+ path("profile/", views.profile_view, name="profile_view"),
+ path("logout/", CustomLogoutView.as_view(), name="logout"), # Logout URL
path("", include("allauth.urls")), # This allows allauth URLs under /accounts/
path(
"password_reset/",
diff --git a/src/accounts/views.py b/src/accounts/views.py
index d8dd469..37ce682 100644
--- a/src/accounts/views.py
+++ b/src/accounts/views.py
@@ -5,7 +5,7 @@
from axes.models import AccessAttempt
from django.conf import settings
from django.contrib.auth import login
-from django.contrib.auth.views import LoginView
+from django.contrib.auth.views import LoginView, LogoutView
from django.core.exceptions import PermissionDenied
from django.db import models
from django.shortcuts import get_object_or_404, render, redirect
@@ -255,6 +255,27 @@ def get_success_url(self):
return reverse_lazy("login")
+class CustomLogoutView(LogoutView):
+ def get_next_page(self):
+ # Get the default next page
+ next_page = super().get_next_page()
+ user = self.request.user
+
+ # Check if the user is authenticated
+ if user.is_authenticated:
+ if user.user_type == "service_provider":
+ # Redirect service providers to the service provider login page
+ next_page = reverse_lazy("service_provider_login")
+ else:
+ # Redirect normal users to the home page
+ next_page = reverse_lazy("home")
+ else:
+ # If the user is not authenticated, default to home page
+ next_page = reverse_lazy("home")
+
+ return next_page
+
+
# Login selection page view
def login_selection(request):
return render(request, "login_selection.html")
diff --git a/src/home/templates/partials/_table.html b/src/home/templates/partials/_table.html
index c114c03..a34c969 100644
--- a/src/home/templates/partials/_table.html
+++ b/src/home/templates/partials/_table.html
@@ -174,496 +174,30 @@
Service NameLeave a Review
-
-
-
+
+
+
+
+
+
+
+
-
+
diff --git a/src/home/urls.py b/src/home/urls.py
index cb906d3..faf5ec5 100644
--- a/src/home/urls.py
+++ b/src/home/urls.py
@@ -5,13 +5,9 @@
from . import views
urlpatterns = [
- path("", login_required(views.home_view), name="home"),
- path(
- "submit_review/", login_required(views.submit_review), name="submit_review"
- ), # New URL
- path(
- "get_reviews//", views.get_reviews, name="get_reviews"
- ), # Fix this path
+ path("", views.home_view, name="home"),
+ path("submit_review/", login_required(views.submit_review), name="submit_review"),
+ path("get_reviews//", views.get_reviews, name="get_reviews"),
path("toggle_bookmark/", views.toggle_bookmark, name="toggle_bookmark"),
path("delete_review//", views.delete_review, name="delete_review"),
path("edit_review//", views.edit_review, name="edit_review"),
diff --git a/src/home/views.py b/src/home/views.py
index e83ca08..abcdc0a 100644
--- a/src/home/views.py
+++ b/src/home/views.py
@@ -35,6 +35,9 @@ def convert_decimals(obj):
@require_POST
def submit_review(request):
+ if not request.user.is_authenticated:
+ return JsonResponse({"error": "Authentication required."}, status=401)
+
try:
data = json.loads(request.body)
service_id = data.get("service_id")
@@ -232,6 +235,9 @@ def get_reviews(request, service_id):
@require_POST
def toggle_bookmark(request):
+ if not request.user.is_authenticated:
+ return JsonResponse({"error": "Authentication required."}, status=401)
+
try:
data = json.loads(request.body)
service_id = data.get("service_id")
@@ -265,6 +271,9 @@ def toggle_bookmark(request):
@require_http_methods(["DELETE"])
def delete_review(request, review_id):
+ if not request.user.is_authenticated:
+ return JsonResponse({"error": "Authentication required."}, status=401)
+
try:
repo = HomeRepository()
data = json.loads(request.body)
@@ -285,6 +294,9 @@ def delete_review(request, review_id):
@require_http_methods(["PUT"])
def edit_review(request, review_id):
+ if not request.user.is_authenticated:
+ return JsonResponse({"error": "Authentication required."}, status=401)
+
try:
data = json.loads(request.body)
new_rating = data.get("rating")
diff --git a/src/public_service_finder/views.py b/src/public_service_finder/views.py
index 3ba12d5..7983a58 100644
--- a/src/public_service_finder/views.py
+++ b/src/public_service_finder/views.py
@@ -9,13 +9,13 @@
def root_redirect_view(request):
if request.user.is_authenticated:
- return (
- redirect("services:list")
- if request.user.user_type == "service_provider"
- else redirect("home")
- )
+ if request.user.user_type == "service_provider":
+ return redirect("services:list")
+ else:
+ return redirect("home")
else:
- return redirect("user_login") # Redirect to user login if not logged in
+ # If the user is not authenticated, redirect to home page
+ return redirect("home") # Redirect to user login if not logged in
@login_required
diff --git a/src/services/urls.py b/src/services/urls.py
index 7d1f3c2..6657632 100644
--- a/src/services/urls.py
+++ b/src/services/urls.py
@@ -4,7 +4,7 @@
app_name = "services"
urlpatterns = [
- path("", views.service_list, name="list"),
+ path("list/", views.service_list, name="list"),
path("create/", views.service_create, name="create"),
path("/edit/", views.service_edit, name="edit"),
path("/delete/", views.service_delete, name="delete"),
diff --git a/src/static/js/home.js b/src/static/js/home.js
new file mode 100644
index 0000000..c0f21aa
--- /dev/null
+++ b/src/static/js/home.js
@@ -0,0 +1,145 @@
+// home.js
+
+// Get CSRF token from cookie
+function getCookie(name) {
+ let cookieValue = null;
+ if (document.cookie && document.cookie !== '') {
+ const cookies = document.cookie.split(';');
+ for (let cookie of cookies) {
+ cookie = cookie.trim();
+ if (cookie.startsWith(name + '=')) {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
+ }
+ }
+ }
+ return cookieValue;
+}
+const csrfToken = getCookie('csrftoken');
+
+// Function to show the login modal
+function showLoginModal() {
+ const loginModal = document.getElementById('loginModal');
+ if (loginModal) {
+ loginModal.classList.remove('hidden');
+ }
+}
+
+// Function to hide the login modal
+function hideLoginModal() {
+ const loginModal = document.getElementById('loginModal');
+ if (loginModal) {
+ loginModal.classList.add('hidden');
+ }
+}
+
+// Event listener for DOM content loaded
+document.addEventListener('DOMContentLoaded', function() {
+ // Handle login form submission
+ const loginForm = document.getElementById('loginForm');
+ if (loginForm) {
+ loginForm.addEventListener('submit', function(event) {
+ event.preventDefault();
+
+ const username = document.getElementById('username').value;
+ const password = document.getElementById('password').value;
+
+ // Perform AJAX login
+ fetch('/ajax_login/', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': csrfToken,
+ 'X-Requested-With': 'XMLHttpRequest',
+ },
+ body: JSON.stringify({
+ username: username,
+ password: password,
+ }),
+ })
+ .then(response => {
+ if (response.ok) {
+ hideLoginModal();
+ // Optionally refresh the page or update the UI to reflect the logged-in state
+ location.reload();
+ } else {
+ return response.json().then(data => {
+ alert(data.error || 'Login failed. Please try again.');
+ });
+ }
+ })
+ .catch(error => {
+ console.error('Login error:', error);
+ alert('An error occurred during login. Please try again.');
+ });
+ });
+ }
+
+ // Handle cancel button
+ const cancelLoginBtn = document.getElementById('cancelLogin');
+ if (cancelLoginBtn) {
+ cancelLoginBtn.addEventListener('click', function() {
+ hideLoginModal();
+ });
+ }
+
+ // Handle close button
+ const closeLoginModalBtn = document.getElementById('closeLoginModal');
+ if (closeLoginModalBtn) {
+ closeLoginModalBtn.addEventListener('click', function() {
+ hideLoginModal();
+ });
+ }
+
+ // Example: Handle user-specific actions (e.g., bookmarking)
+ const bookmarkButtons = document.querySelectorAll('.bookmark-btn');
+ bookmarkButtons.forEach(button => {
+ button.addEventListener('click', function() {
+ const serviceId = this.dataset.serviceId;
+ const action = this.dataset.action; // 'add' or 'remove'
+
+ toggleBookmark(serviceId, action, this);
+ });
+ });
+
+ // Function to toggle bookmark
+ function toggleBookmark(serviceId, action, button) {
+ fetch('/toggle_bookmark/', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': csrfToken,
+ 'X-Requested-With': 'XMLHttpRequest',
+ },
+ body: JSON.stringify({
+ service_id: serviceId,
+ action: action,
+ }),
+ })
+ .then(response => {
+ if (response.status === 401) {
+ // User is not authenticated, show login modal
+ showLoginModal();
+ } else if (response.ok) {
+ return response.json().then(data => {
+ // Update bookmark UI
+ if (data.action === 'added') {
+ button.dataset.action = 'remove';
+ button.textContent = 'Unbookmark';
+ } else if (data.action === 'removed') {
+ button.dataset.action = 'add';
+ button.textContent = 'Bookmark';
+ }
+ });
+ } else {
+ return response.json().then(data => {
+ alert(data.error || 'Failed to toggle bookmark.');
+ });
+ }
+ })
+ .catch(error => {
+ console.error('Toggle bookmark error:', error);
+ alert('An error occurred while toggling the bookmark.');
+ });
+ }
+});
diff --git a/src/static/js/profile.js b/src/static/js/profile.js
index bfe7062..d48673a 100644
--- a/src/static/js/profile.js
+++ b/src/static/js/profile.js
@@ -432,7 +432,7 @@ document.addEventListener('DOMContentLoaded', () => {
alert(data.error || 'Failed to toggle bookmark.');
} else {
if (action === 'remove') {
- const serviceCard = this.closest('.bg-gray-50');
+ const serviceCard = this.closest('.bg-gray-700');
serviceCard.remove();
// Update counters after removing bookmark
diff --git a/src/static/js/table.js b/src/static/js/table.js
new file mode 100644
index 0000000..5c6379b
--- /dev/null
+++ b/src/static/js/table.js
@@ -0,0 +1,488 @@
+document.addEventListener('DOMContentLoaded', () => {
+
+ const reviewFormContainer = document.getElementById('reviewFormContainer');
+ const loginToReviewContainer = document.getElementById('loginToReviewContainer');
+
+ if (userIsAuthenticated) {
+ reviewFormContainer.style.display = 'block';
+ loginToReviewContainer.style.display = 'none';
+ } else {
+ reviewFormContainer.style.display = 'none';
+ loginToReviewContainer.style.display = 'block';
+
+ const loginButton = document.getElementById('loginToReview');
+ loginButton.addEventListener('click', () => {
+ window.location.href = '/accounts/login/user/';
+ });
+ }
+
+ function formatTimestamp(utcTimestamp) {
+ let dateString = utcTimestamp;
+
+ // Check if the timestamp already contains timezone info (Z or ±HH:MM)
+ const hasTimezone = /([Zz]|[+\-]\d{2}:\d{2})$/.test(utcTimestamp);
+
+ if (!hasTimezone) {
+ // Append 'Z' only if no timezone info is present
+ dateString += 'Z';
+ }
+
+ const date = new Date(dateString);
+
+ // Check if the date is valid
+ if (isNaN(date)) {
+ console.error("Invalid date:", dateString);
+ return "Invalid Date";
+ }
+
+ const options = {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: true,
+ };
+
+ return date.toLocaleString(undefined, options);
+ }
+
+ function sanitizeHTML(str) {
+ const tempDiv = document.createElement('div');
+ tempDiv.textContent = str; // Escapes the string
+ return tempDiv.innerHTML; // Returns the escaped string
+ }
+
+ async function fetchAndDisplayReviews(serviceId, page = 1) {
+
+ try {
+ const response = await fetch(`/home/get_reviews/${serviceId}/?page=${page}`);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch reviews. Status: ${response.status}`);
+ }
+
+ const {reviews, has_next, has_previous, current_page, username} = await response.json();
+
+ const reviewsContainer = document.getElementById('reviewsContainer');
+ reviewsContainer.innerHTML = '';
+
+ if (reviews.length === 0) {
+ reviewsContainer.innerHTML = 'No reviews yet.
';
+ return;
+ }
+
+ reviews.forEach(review => {
+ const reviewDiv = document.createElement('div');
+ reviewDiv.classList.add('bg-gray-800', 'rounded', 'shadow', 'p-4', 'mb-4');
+ const rating = parseFloat(review.RatingStars).toFixed(2);
+
+ const flexContainer = document.createElement('div');
+ flexContainer.classList.add('flex-container'); // Add custom class for styling
+
+ // Create and configure the rating element
+ const ratingElement = document.createElement('p');
+ ratingElement.classList.add('text-yellow-400', 'font-semibold', 'rating-element');
+ ratingElement.textContent = `${rating} ★`;
+
+ // Append the rating element to the flex container
+ flexContainer.appendChild(ratingElement);
+
+ // Check if username matches
+ if (username === review.Username) {
+ // Create a container for the icons
+ const iconContainer = document.createElement('div');
+ iconContainer.classList.add('icon-container'); // Add custom class for styling
+
+ if (!review.ResponseText) {
+ const editIcon = document.createElement('i');
+ editIcon.classList.add('fas', 'fa-edit', 'text-blue-500', 'cursor-pointer');
+ editIcon.title = "Edit Review";
+ editIcon.onclick = () => handleEditReview(review);
+ iconContainer.appendChild(editIcon);
+ }
+ // Delete icon
+ const deleteIcon = document.createElement('i');
+ deleteIcon.classList.add('fas', 'fa-trash', 'text-red-500', 'cursor-pointer');
+ deleteIcon.title = "Delete Review";
+ deleteIcon.onclick = () => handleDeleteReview(review);
+ iconContainer.appendChild(deleteIcon);
+
+ // Append the icon container to the flex container
+ flexContainer.appendChild(iconContainer);
+ }
+
+ // Append the flex container to the reviewDiv
+ reviewDiv.appendChild(flexContainer);
+
+
+ const reviewText = document.createElement('p');
+ reviewText.classList.add('text-sm');
+ reviewText.innerHTML = sanitizeHTML(review.RatingMessage).replace(/\n/g, '
');
+ reviewDiv.appendChild(reviewText);
+
+ const timestamp = formatTimestamp(review.Timestamp);
+ const meta = document.createElement('p');
+ meta.classList.add('text-sm', 'text-gray-400');
+ meta.textContent = `By ${review.Username} on ${timestamp}`;
+ reviewDiv.appendChild(meta);
+
+ if (review.ResponseText) {
+ const responseDiv = document.createElement('div');
+ responseDiv.classList.add('mt-2', 'p-3', 'bg-gray-700', 'border-gray-800', 'rounded');
+
+ const responseHeader = document.createElement('p');
+ responseHeader.classList.add('font-semibold', 'text-sm', 'text-blue-500');
+ responseHeader.textContent = "Provider Response:";
+ responseDiv.appendChild(responseHeader);
+
+ const responseText = document.createElement('p');
+ responseText.classList.add('text-sm');
+ responseText.innerHTML = sanitizeHTML(review.ResponseText).replace(/\n/g, '
');
+ responseDiv.appendChild(responseText);
+
+ const respondedAt = formatTimestamp(review.RespondedAt);
+ const responseMeta = document.createElement('p');
+ responseMeta.classList.add('text-xs', 'text-gray-400');
+ responseMeta.textContent = `Responded on ${respondedAt}`;
+ responseDiv.appendChild(responseMeta);
+
+ reviewDiv.appendChild(responseDiv);
+ }
+
+ reviewsContainer.appendChild(reviewDiv);
+ });
+
+ // Pagination controls
+ const paginationDiv = document.createElement('div');
+ paginationDiv.classList.add('flex', 'justify-between', 'mt-4');
+
+ if (has_previous) {
+ const prevButton = document.createElement('button');
+ prevButton.classList.add('bg-blue-500', 'text-white', 'px-4', 'py-2', 'rounded');
+ prevButton.textContent = 'Previous';
+ prevButton.addEventListener('click', () => fetchAndDisplayReviews(serviceId, current_page - 1));
+ paginationDiv.appendChild(prevButton);
+ }
+
+ if (has_next) {
+ const nextButton = document.createElement('button');
+ nextButton.classList.add('bg-blue-500', 'text-white', 'px-4', 'py-2', 'rounded');
+ nextButton.textContent = 'Next';
+ nextButton.addEventListener('click', () => fetchAndDisplayReviews(serviceId, current_page + 1));
+ paginationDiv.appendChild(nextButton);
+ }
+
+ reviewsContainer.appendChild(paginationDiv);
+ } catch (error) {
+ console.error('Error fetching reviews:', error);
+ alert('Failed to load reviews. Please try again.');
+ }
+ }
+
+ // Expose the function globally
+ window.fetchAndDisplayReviews = fetchAndDisplayReviews;
+
+ // Add the showServiceDetails function
+ function showServiceDetails(index) {
+ const service = itemsData.find(item => item.Id === index);
+ if (!service) {
+ console.error(`Service with index ${index} not found.`);
+ return;
+ }
+
+ document.getElementById('reviewRating').value = '';
+ document.getElementById('reviewText').value = '';
+
+ // Populate basic service details
+ document.getElementById('serviceId').textContent = service.Id || 'No ID';
+ document.getElementById('serviceName').textContent = service.Name || 'No Name';
+ document.getElementById('serviceAddress').textContent = service.Address || 'N/A';
+ document.getElementById('serviceType').textContent = service.Category || 'Unknown';
+ document.getElementById('serviceRating').textContent = service.Ratings && service.Ratings !== 0 ? parseFloat(service.Ratings).toFixed(2) : 'N/A';
+ document.getElementById('serviceDistance').textContent = service.Distance ? parseFloat(service.Distance).toFixed(2) + ' miles' : 'N/A';
+
+ const announcementDiv = document.getElementById('serviceAnnouncement');
+ const announcementText = announcementDiv.querySelector('p');
+ if (service.Announcement && service.Announcement.trim()) {
+ announcementText.textContent = service.Announcement;
+ announcementDiv.classList.remove('hidden');
+ } else {
+ announcementDiv.classList.add('hidden');
+ }
+
+ const serviceAvailability = document.getElementById('serviceAvailability');
+ if (service.IsActive === false) {
+ serviceAvailability.textContent = 'Currently Unavailable';
+ serviceAvailability.classList.remove('text-green-500');
+ serviceAvailability.classList.add('text-red-500');
+ } else {
+ serviceAvailability.textContent = 'Available';
+ serviceAvailability.classList.remove('text-red-500');
+ serviceAvailability.classList.add('text-green-500');
+ }
+ serviceAvailability.classList.remove('hidden');
+
+
+ const bookmarkCheckbox = document.getElementById('bookmarkCheckbox');
+
+ if (bookmarkCheckbox && userIsAuthenticated) {
+
+ // Set the initial checkbox state based on the bookmark status
+ bookmarkCheckbox.checked = service.IsBookmarked;
+
+
+ // Add event listener to the bookmark checkbox
+ bookmarkCheckbox.onchange = function () {
+ const action = bookmarkCheckbox.checked ? 'add' : 'remove';
+ const serviceId = service.Id;
+
+ fetch('/home/toggle_bookmark/', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': getCsrfToken(),
+ },
+ body: JSON.stringify({
+ 'service_id': serviceId,
+ 'action': action,
+ }),
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ service.IsBookmarked = bookmarkCheckbox.checked;
+ } else {
+ // Revert the checkbox state if the request failed
+ bookmarkCheckbox.checked = !bookmarkCheckbox.checked;
+ alert(data.error || 'Failed to toggle bookmark.');
+ }
+ })
+ .catch(error => {
+ console.error('Error toggling bookmark:', error);
+ // Revert the checkbox state if an error occurred
+ bookmarkCheckbox.checked = !bookmarkCheckbox.checked;
+ alert('An error occurred. Please try again.');
+ });
+ };
+ } else if (bookmarkCheckbox) {
+ // Hide the bookmark checkbox if the user is not authenticated
+ bookmarkCheckbox.parentElement.style.display = true;
+ }
+
+
+ const descriptionElement = document.getElementById('serviceDescription');
+ descriptionElement.innerHTML = ''; // Clear previous content
+
+ const heading = document.createElement('h3');
+ heading.textContent = 'Additional Descriptive Details:';
+ heading.style.marginBottom = '10px';
+ heading.style.marginTop = '20px';
+ heading.style.fontSize = '1.1em';
+ heading.style.fontWeight = 'bold';
+ descriptionElement.appendChild(heading);
+
+ // Check if Description exists and is an object
+ if (service.Description && typeof service.Description === 'object') {
+ let hasDescription = false;
+ const dl = document.createElement('dl');
+ dl.className = 'mt-2 space-y-1';
+
+ for (const [key, value] of Object.entries(service.Description)) {
+ if (value !== null && value !== '') {
+ hasDescription = true;
+ const div = document.createElement('div');
+ div.className = 'flex';
+
+ const dt = document.createElement('dt');
+ dt.className = 'text-sm font-medium text-gray-400 w-1/3';
+ dt.textContent = `${key.replace(/_/g, ' ')}:`;
+
+ const dd = document.createElement('dd');
+ dd.className = 'text-sm text-gray-300 ml-2';
+ dd.innerHTML = value.replace(/\n/g, '
');
+
+ div.appendChild(dt);
+ div.appendChild(dd);
+ dl.appendChild(div);
+ }
+ }
+
+ if (hasDescription) {
+ descriptionElement.appendChild(dl);
+ } else {
+ descriptionElement.textContent = 'No description available.';
+ }
+ } else {
+ descriptionElement.textContent = 'No description available.';
+ }
+
+ const getDirectionsBtn = document.getElementById('getDirections');
+
+ if (service.MapLink) {
+ getDirectionsBtn.href = service.MapLink;
+ getDirectionsBtn.style.display = 'inline-block';
+ } else {
+ getDirectionsBtn.href = '#';
+ getDirectionsBtn.style.display = 'none';
+ }
+
+ // Fetch and display reviews
+ fetchAndDisplayReviews(service.Id, 1);
+
+ const serviceDetailsDiv = document.getElementById('serviceDetails');
+ if (serviceDetailsDiv) {
+ serviceDetailsDiv.classList.remove('hidden');
+ serviceDetailsDiv.classList.add('block');
+ }
+ }
+
+ // Expose the function globally
+ window.showServiceDetails = showServiceDetails;
+
+ // Event listener for closeDetails button
+ document.getElementById('closeDetails').addEventListener('click', () => {
+ const serviceDetailsDiv = document.getElementById('serviceDetails');
+ if (serviceDetailsDiv) {
+ serviceDetailsDiv.classList.add('hidden');
+ serviceDetailsDiv.classList.remove('block');
+ }
+ });
+
+ async function handleEditReview(review) {
+ const modal = document.getElementById('editReviewModal');
+ const editRating = document.getElementById('editRating');
+ const editMessage = document.getElementById('editMessage');
+
+ // Set current values
+ editRating.value = review.RatingStars;
+ editMessage.value = review.RatingMessage;
+
+ modal.classList.remove('hidden');
+
+ const handleEdit = async () => {
+ try {
+ const response = await fetch(
+ `/home/edit_review/${review.ReviewId}/`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ "X-CSRFToken": getCsrfToken(),
+ },
+ body: JSON.stringify({
+ username: review.Username,
+ rating: parseInt(editRating.value),
+ message: editMessage.value.trim(),
+ }),
+ }
+ );
+
+ const data = await response.json();
+ if (response.ok && data.success) {
+ fetchAndDisplayReviews(review.ServiceId, 1);
+ modal.classList.add('hidden');
+ } else {
+ alert(data.error || "Failed to edit review.");
+ }
+ } catch (error) {
+ console.error("Error editing review:", error);
+ alert("An error occurred. Please try again.");
+ }
+ };
+
+ // Event listeners
+ document.getElementById('confirmEdit').onclick = handleEdit;
+ document.getElementById('cancelEdit').onclick = () => {
+ modal.classList.add('hidden');
+ };
+ }
+
+ function handleDeleteReview(review) {
+ const modal = document.getElementById('deleteReviewModal');
+ modal.classList.remove('hidden');
+
+ const handleDelete = async () => {
+ try {
+ const response = await fetch(`/home/delete_review/${review.ReviewId}/`, {
+ method: "DELETE",
+ headers: {
+ "X-CSRFToken": getCsrfToken(),
+ },
+ body: JSON.stringify({
+ username: review.Username,
+ }),
+ });
+
+ const data = await response.json();
+ if (data.success) {
+ await fetchAndDisplayReviews(review.ServiceId, 1);
+ modal.classList.add('hidden');
+ } else {
+ alert(data.error || "Failed to delete review.");
+ }
+ } catch (error) {
+ console.error("Error deleting review:", error);
+ alert("An error occurred. Please try again.");
+ }
+ };
+
+ // Event listeners
+ document.getElementById('confirmDelete').onclick = handleDelete;
+ document.getElementById('cancelDelete').onclick = () => {
+ modal.classList.add('hidden');
+ };
+ }
+
+ // Event listener for submitReview button
+ document.getElementById('submitReview').addEventListener('click', async () => {
+ const rating = document.getElementById('reviewRating').value;
+ const message = document.getElementById('reviewText').value;
+ const serviceId = document.getElementById('serviceId').textContent.trim();
+
+ if (rating === "" || message.trim() === "") {
+ alert("Please provide both a rating and a message.");
+ return;
+ }
+
+ const reviewData = {
+ service_id: serviceId,
+ rating: parseInt(rating),
+ message: message,
+ };
+
+ try {
+ const response = await fetch('/home/submit_review/', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': getCsrfToken(),
+ },
+ body: JSON.stringify(reviewData),
+ });
+
+ if (response.ok) {
+ const result = await response.json();
+ fetchAndDisplayReviews(serviceId, 1);
+ document.getElementById('reviewRating').value = '';
+ document.getElementById('reviewText').value = '';
+ } else {
+ const error = await response.json();
+ alert(error.error || "Failed to submit review.");
+ }
+ } catch (error) {
+ console.error('Error submitting review:', error);
+ alert("An error occurred. Please try again.");
+ }
+ });
+
+});
+
+// Function to get CSRF token from cookies
+function getCsrfToken() {
+ const cookies = document.cookie.split('; ');
+ for (const cookie of cookies) {
+ const [name, value] = cookie.split('=');
+ if (name === 'csrftoken') return value;
+ }
+ return '';
+}