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 ''; +}