Skip to content

Commit

Permalink
Implement unread notifications filter and enhance NotificationViewSet (
Browse files Browse the repository at this point in the history
…#105)

* feat: implement unread notifications filter in NotificationViewSet

* refactor: Improve notifications
  • Loading branch information
drikusroor authored Oct 12, 2024
1 parent 2d73fa9 commit 4304b9d
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 25 deletions.
1 change: 0 additions & 1 deletion src/animals/static/js/add_animal.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ document.addEventListener('DOMContentLoaded', function() {
})
.then(response => response.json())
.then(data => {
console.log('Animal added:', data);
// You can add some feedback here, like a toast notification
showToast('Animal added successfully!');
})
Expand Down
8 changes: 0 additions & 8 deletions src/blog/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,7 @@
router.register(r"likes", LikeViewSet)
router.register(r"comment-likes", CommentLikeViewSet, basename="comment-likes")


# path('api/posts-by-date/', posts_by_date, name='posts_by_date'),
# router.register(r"posts-by-date", posts_by_date, basename="posts-by-date")

urlpatterns = [
path(
"api/",
include("notifications.urls"),
),
path("api/", include(router.urls)),
path("api/posts-by-date/", posts_by_date, name="posts_by_date"),
path(
Expand Down
23 changes: 23 additions & 0 deletions src/brazil_blog/static/css/brazil_blog_compiled.css
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,10 @@ select {
margin-top: 1rem;
}

.mt-6 {
margin-top: 1.5rem;
}

.mt-8 {
margin-top: 2rem;
}
Expand Down Expand Up @@ -1725,6 +1729,10 @@ select {
min-height: 12rem;
}

.min-h-96 {
min-height: 24rem;
}

.min-h-screen {
min-height: 100vh;
}
Expand Down Expand Up @@ -2398,6 +2406,10 @@ select {
padding-bottom: 1rem;
}

.pb-8 {
padding-bottom: 2rem;
}

.pl-0 {
padding-left: 0px;
}
Expand Down Expand Up @@ -2513,6 +2525,11 @@ select {
color: rgb(243 244 246 / var(--tw-text-opacity));
}

.text-gray-400 {
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity));
}

.text-gray-500 {
--tw-text-opacity: 1;
color: rgb(107 114 128 / var(--tw-text-opacity));
Expand Down Expand Up @@ -2590,6 +2607,12 @@ select {
opacity: 0.5;
}

.shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}

.shadow-lg {
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
Expand Down
1 change: 1 addition & 0 deletions src/brazil_blog/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
path("locations/", include("locations.urls")),
path("drinks/", include(drinks_urls)),
path("animals/", include(animals_urls)),
path("notifications/", include("notifications.urls")),
]


Expand Down
1 change: 0 additions & 1 deletion src/drinks/static/js/add_drink.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ document.addEventListener('DOMContentLoaded', function() {
})
.then(response => response.json())
.then(data => {
console.log('Drink added:', data);
// You can add some feedback here, like a toast notification
showToast('Drink added successfully!');
})
Expand Down
2 changes: 0 additions & 2 deletions src/drinks/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,6 @@ def get_context_data(self, **kwargs):
today_date = date.today()
total_days_amount = (today_date - start_date).days + 1

print(total_days_amount)

# Average drinks per day per type
avg_drinks = (
DrinkType.objects.all()
Expand Down
103 changes: 103 additions & 0 deletions src/notifications/templates/notifications/all-notifications.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
{% extends "base.html" %} {% load static %} {% block content %}
<div class="bg-green-700">
<div class="container mx-auto px-4 py-8 min-h-96">
<h1 class="text-3xl font-bold mb-6">All Notifications</h1>

<div id="all-notifications" class="space-y-4">
<!-- Notifications will be dynamically inserted here -->
</div>

<button
id="mark-all-read"
class="mt-6 bg-blue-500 text-white rounded-md py-2 px-4 hover:bg-blue-600 transition duration-200"
>
Mark all as read
</button>
</div>
</div>

<script>
document.addEventListener("DOMContentLoaded", function () {
const allNotifications = document.getElementById("all-notifications");
const markAllReadButton = document.getElementById("mark-all-read");

function fetchAllNotifications() {
fetch("/notifications/api/notifications/all/")
.then((response) => response.json())
.then((data) => {
allNotifications.innerHTML = "";

data.forEach((notification) => {
const item = document.createElement("div");
item.className = "p-4 bg-white shadow rounded-lg";
item.innerHTML = `
<a
data-notification-link
href="${
notification.url
}" class="block font-semibold mb-1 hover:underline ${
notification.read ? "text-gray-500" : ""
}">${notification.title}</a>
<p class="text-sm text-gray-600">${
notification.message
}</p>
<p class="text-xs text-gray-400 mt-2">${new Date(
notification.created_at
).toLocaleString()}</p>
${
!notification.read
? `<button class="mt-2 text-sm text-blue-500 hover:underline mark-read" data-id="${notification.id}">Mark as read</button>`
: ""
}
`;
allNotifications.appendChild(item);
});
});
}

allNotifications.addEventListener("click", (e) => {
if (e.target.attributes.getNamedItem("data-notification-link")) {
e.preventDefault();
const link = e.target;
const href = link.getAttribute("href");
const notificationId = link.parentElement.querySelector("button").dataset.id;

fetch(
`/notifications/api/notifications/${notificationId}/mark_as_read/`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": getCookie("csrftoken"),
},
}
).finally(() => (document.location.href = href));
}

if (e.target.classList.contains("mark-read")) {
const id = e.target.dataset.id;
fetch(`/notifications/api/notifications/${id}/mark_as_read/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": getCookie("csrftoken"),
},
}).then(() => fetchAllNotifications());
}
});

markAllReadButton.addEventListener("click", () => {
fetch("/notifications/api/notifications/mark_all_as_read/", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": getCookie("csrftoken"),
},
}).then(() => fetchAllNotifications());
});

// Initial fetch
fetchAllNotifications();
});
</script>
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</svg>
<span id="notification-count" class="absolute top-1 right-1 inline-flex items-center justify-center p-1 w-6 h-6 flex-0 text-xs font-bold leading-none text-red-100 transform translate-x-1/2 -translate-y-1/2 bg-red-600 rounded-full"></span>
</button>
<div id="notification-list" class="hidden absolute right-0 mt-2 w-80 bg-white rounded-3xl shadow-lg overflow-hidden z-30 max-h-96 overflow-y-auto">
<div id="notification-list" class="hidden absolute right-0 mt-2 pb-8 w-80 bg-white rounded-3xl shadow-lg overflow-hidden z-30 max-h-96 overflow-y-auto">
<div class="py-2">
<!-- Notifications will be inserted here -->
</div>
Expand All @@ -26,7 +26,7 @@
const list = document.getElementById('notification-list');

function fetchNotifications() {
fetch('/blog/api/notifications/')
fetch('/notifications/api/notifications/')
.then(response => response.json())
.then(data => {
const unreadCount = data.filter(n => !n.read).length;
Expand All @@ -35,7 +35,7 @@

list.innerHTML = data.map(notification => `
<div class="px-4 py-2 hover:bg-gray-100 ${notification.read ? 'bg-gray-50' : ''}">
<a href="${notification.url}" class="block text-sm mb-1 ${notification.read ? 'text-gray-500' : ''}">${notification.title}</a>
<a data-notification-link href="${notification.url}" class="block text-sm mb-1 hover:underline ${notification.read ? 'text-gray-500' : ''}">${notification.title}</a>
<p class="text-xs text-gray-500">${new Date(notification.created_at).toLocaleString()}</p>
${!notification.read ? `
<button id="markAsRead" data-notification-id="${notification.id}" class="mt-1 text-xs text-indigo-600 hover:text-indigo-900">
Expand All @@ -47,36 +47,62 @@

// Add mark all as read button
list.innerHTML += `
<div class="px-4 py-2 bg-gray-100">
<div class="flex items-center justify-between px-4 py-2 bg-gray-100 absolute w-full left-0 bottom-0">
<button id="markAllAsRead" class="text-xs text-indigo-600 hover:text-indigo-900">
Mark all as read
Mark all as read (${unreadCount})
</button>
<a href="/notifications/" class="text-xs text-indigo-600 hover:text-indigo-900">
View all notifications &rarr;
</a>
</div>
`;

list.querySelectorAll('[data-notification-link]').forEach((link) => {
link.addEventListener('click', onNotificationLinkClick);
});

list.querySelectorAll('#markAsRead').forEach(button => {
button.addEventListener('click', function() {
markAsRead(button.dataset.notificationId);
markAsReadAndFetch(button.dataset.notificationId);
});
});

list.querySelector('#markAllAsRead').addEventListener('click', markAllAsRead);
});
}

function onNotificationLinkClick(event) {
event.preventDefault();

const link = event.target;
const url = link.href;
const button = link.parentElement.querySelector('button');
const id = button.dataset.notificationId;

markAsRead(id)
.then(() => { document.location.href = url; })
.catch(() => { document.location.href = url; });
}

function markAsRead(id) {
fetch(`/blog/api/notifications/${id}/mark_as_read/`, {
return fetch(`/notifications/api/notifications/${id}/mark_as_read/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
}
})
.then(() => fetchNotifications());
});
}


function markAsReadAndFetch(id) {
return markAsRead(id)
.then(() => fetchNotifications())
.catch(() => fetchNotifications());
}

function markAllAsRead() {
fetch(`/blog/api/notifications/mark_all_as_read/`, {
fetch(`/notifications/api/notifications/mark_all_as_read/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
8 changes: 7 additions & 1 deletion src/notifications/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
from django.urls import path, include
from django.views.generic import TemplateView
from rest_framework.routers import DefaultRouter
from .views import NotificationViewSet

router = DefaultRouter()
router.register(r"notifications", NotificationViewSet, basename="notification")

urlpatterns = [
path("", include(router.urls)),
path(
"",
TemplateView.as_view(template_name="notifications/all-notifications.html"),
name="all_notifications",
),
path("api/", include(router.urls)),
]
14 changes: 12 additions & 2 deletions src/notifications/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class NotificationViewSet(viewsets.ModelViewSet):

def list(self, request):
logger.debug("NotificationViewSet.list() called")
queryset = self.get_queryset()
queryset = self.get_unread_queryset()
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

Expand All @@ -23,6 +23,10 @@ def get_queryset(self):
"-created_at"
)

def get_unread_queryset(self):
logger.debug(f"get_unread_queryset called for user: {self.request.user}")
return self.get_queryset().filter(read=False)

@action(detail=True, methods=["post"])
def mark_as_read(self, request, pk=None):
logger.debug(f"mark_as_read called for notification {pk}")
Expand All @@ -31,9 +35,15 @@ def mark_as_read(self, request, pk=None):
notification.save()
return Response({"status": "notification marked as read"})

# mark all as read for the current user
@action(detail=False, methods=["post"])
def mark_all_as_read(self, request):
logger.debug("mark_all_as_read called")
Notification.objects.filter(user=request.user).update(read=True)
return Response({"status": "all notifications marked as read"})

@action(detail=False, methods=["get"])
def all(self, request):
logger.debug("all_notifications called")
queryset = self.get_queryset()
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

0 comments on commit 4304b9d

Please sign in to comment.