Enhance RSVP functionality with live updates and page activation
- Refactored rsvp.js to implement a WebSocket connection for real-time updates on activity attendees and capacity. - Introduced a page activation mechanism to handle dynamic content loading based on the current URL. - Updated base.html to include jQuery for easier DOM manipulation. - Modified event_rsvp.html to pass the RSVP ID to the JavaScript context. - Enhanced rsvp_page_activities.html to display a live connection status and included guest count in the script. - Adjusted rsvp_page_activities_details.html to reflect the attending status in the activity card.
This commit is contained in:
@@ -41,4 +41,10 @@ Het plaatje en de beschrijving van het event moeten misschien tussendoor nog erb
|
||||
Als in: invite link -> event page (create rsvp & token, store in cookie) -> Knop ik kom!, Knop ik kom niet? -> Redirect naar RSVP flow
|
||||
|
||||
## Zeven
|
||||
De websocket overschrijft de huidige keuze. Dus als iets vol zit, wordt hij grijs, ondanks dat die er zelf ook heen gaan en hij groen had moeten zijn!
|
||||
De websocket overschrijft de huidige keuze. Dus als iets vol zit, wordt hij grijs, ondanks dat die er zelf ook heen gaan en hij groen had moeten zijn!
|
||||
|
||||
## Acht
|
||||
Mails!
|
||||
|
||||
## Negen
|
||||
Gasten (template): koppelen aan een invite!
|
||||
+46
-2
@@ -1,15 +1,22 @@
|
||||
import json
|
||||
import logging
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
connected_websocket_consumers = 0
|
||||
|
||||
class ActivityConsumer(AsyncWebsocketConsumer):
|
||||
async def connect(self):
|
||||
global connected_websocket_consumers
|
||||
|
||||
if "url_route" not in self.scope:
|
||||
raise ValueError
|
||||
|
||||
self.event_id = self.scope["url_route"]["kwargs"]["event_id"]
|
||||
|
||||
logger.debug(f"Connection for Event={self.event_id}")
|
||||
|
||||
# Subscribe to database changes for this event
|
||||
await self.channel_layer.group_add(
|
||||
f"activity_{self.event_id}",
|
||||
@@ -17,18 +24,55 @@ class ActivityConsumer(AsyncWebsocketConsumer):
|
||||
)
|
||||
|
||||
await self.accept()
|
||||
|
||||
connected_websocket_consumers += 1
|
||||
|
||||
logger.debug(f"Websocket for Event={self.event_id} opened ({connected_websocket_consumers} total)")
|
||||
|
||||
|
||||
await self.update_connected()
|
||||
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
global connected_websocket_consumers
|
||||
logger.debug(f"Disconnection for Event={self.event_id}")
|
||||
|
||||
# Remove from group on disconnect
|
||||
await self.channel_layer.group_discard(
|
||||
f"activity_{self.event_id}",
|
||||
self.channel_name
|
||||
)
|
||||
connected_websocket_consumers -= 1
|
||||
|
||||
logger.debug(f"Websocket for Event={self.event_id} closed ({connected_websocket_consumers} total)")
|
||||
|
||||
await self.update_connected()
|
||||
|
||||
|
||||
async def update_connected(self):
|
||||
"""Update all connected """
|
||||
logger.debug("Sending connected update...")
|
||||
|
||||
await self.channel_layer.group_send(
|
||||
f"activity_{self.event_id}",
|
||||
{
|
||||
"type": "group.message",
|
||||
"activity_json": {} # connected will be filled
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def group_message(self, event):
|
||||
global connected_websocket_consumers
|
||||
|
||||
logger.debug(f"Message {self.event_id}: {event}")
|
||||
|
||||
activity_json = event.get("activity_json")
|
||||
logger.debug("Ready to send: " + str(activity_json))
|
||||
if activity_json is not None:
|
||||
activity_json["connected"] = connected_websocket_consumers
|
||||
activity_str = json.dumps(activity_json)
|
||||
|
||||
if activity_json:
|
||||
await self.send(text_data=activity_json)
|
||||
logger.debug("Sending: " + str(activity_str))
|
||||
|
||||
await self.send(text_data=activity_str)
|
||||
+12
-1
@@ -506,7 +506,7 @@ class GuestListTemplate(TimestampedModel):
|
||||
return self.title
|
||||
|
||||
|
||||
class GuestListItem(models.Model):
|
||||
class GuestListItem(TimestampedModel):
|
||||
template = models.ForeignKey(GuestListTemplate, related_name='items', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
email = models.EmailField(blank=True)
|
||||
@@ -514,3 +514,14 @@ class GuestListItem(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class GuestItemInvite(TimestampedModel):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
guest = models.ForeignKey(GuestListItem, related_name="guest_invite", on_delete=models.CASCADE)
|
||||
invite = models.ForeignKey(Invite, related_name="guest_invite", on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
unique_together = (("guest", "invite"),)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.guest} added to {self.invite}"
|
||||
+6
-4
@@ -42,13 +42,14 @@ def activity_selection_changed(sender, instance, created, **kwargs):
|
||||
if capacity is None:
|
||||
capacity = 100
|
||||
|
||||
activity_json = json.dumps({
|
||||
activity_json = {
|
||||
"activity": str(activity.id),
|
||||
"rsvp": str(rsvp.id),
|
||||
"action": "select",
|
||||
"attendees": activity.attendee_count(),
|
||||
"capacity": capacity,
|
||||
"guests": activity.attendee_list()
|
||||
})
|
||||
}
|
||||
|
||||
# Send message to the group
|
||||
async_to_sync(send_channel)(event.id, activity_json)
|
||||
@@ -66,13 +67,14 @@ def activity_selection_deleted(sender, instance, **kwargs):
|
||||
if capacity is None:
|
||||
capacity = 100
|
||||
|
||||
activity_json = json.dumps({
|
||||
activity_json = {
|
||||
"activity": str(activity.id),
|
||||
"rsvp": str(rsvp.id),
|
||||
"action": "remove",
|
||||
"attendees": activity.attendee_count(),
|
||||
"capacity": capacity,
|
||||
"guests": activity.attendee_list()
|
||||
})
|
||||
}
|
||||
|
||||
# Send message to the group
|
||||
async_to_sync(send_channel)(event.id, activity_json)
|
||||
|
||||
+2
File diff suppressed because one or more lines are too long
+136
-39
@@ -1,53 +1,150 @@
|
||||
const activitySocket = new WebSocket(
|
||||
'ws://'
|
||||
+ window.location.host
|
||||
+ '/ws/event/'
|
||||
+ eventId
|
||||
+ '/'
|
||||
);
|
||||
function extractPageFromUrl(url) {
|
||||
// Match /page/<page> pattern
|
||||
const match = url.match(/\/page\/([^\/]+)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
activitySocket.onmessage = function(e) {
|
||||
console.log(e);
|
||||
data = JSON.parse(e.data);
|
||||
$(document).ready(() => {
|
||||
pageActivator();
|
||||
});
|
||||
|
||||
// Activity attendees (when everyone can join)
|
||||
attendees = document.querySelector('#activity-' + data.activity + ' .numatt');
|
||||
if(attendees){
|
||||
attendees.innerHTML = data.attendees;
|
||||
htmx.onLoad(function(content) {
|
||||
pageActivator();
|
||||
});
|
||||
|
||||
var deactivator = () => {};
|
||||
var currentPage = "";
|
||||
|
||||
function pageActivator(){
|
||||
const currentPath = window.location.pathname;
|
||||
var newPage = extractPageFromUrl(currentPath);
|
||||
|
||||
if(newPage == currentPage){
|
||||
return;
|
||||
}
|
||||
|
||||
// Activity capacity (when there is a guest limit)
|
||||
capacity = document.querySelector('#activity-' + data.activity + ' .numcap');
|
||||
if(capacity){
|
||||
capacity.innerHTML = data.capacity;
|
||||
}
|
||||
console.log(`Swapping from ${currentPage} to ${newPage}`);
|
||||
currentPage = newPage;
|
||||
|
||||
// Activity capacity (when there is a guest limit)
|
||||
activity = document.querySelector('#activity-' + data.activity);
|
||||
if(activity){
|
||||
if(data.capacity >= guestCount){
|
||||
activity.classList.remove("bg-gray");
|
||||
activity.classList.remove("bd-gray");
|
||||
// Deactivate the old page
|
||||
deactivator();
|
||||
|
||||
switch (currentPage) {
|
||||
case 'activities':
|
||||
deactivator = activitiesPage();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function activitiesPage() {
|
||||
|
||||
function showLive(number, domElement) {
|
||||
if(!domElement){
|
||||
domElement = $('#rsvp-activity .live');
|
||||
}
|
||||
else{
|
||||
activity.classList.add("bg-gray");
|
||||
activity.classList.add("bd-gray");
|
||||
|
||||
if(domElement && number){
|
||||
switch (number) {
|
||||
case 0:
|
||||
case 1:
|
||||
domElement.html("🔴 Only you");
|
||||
break;
|
||||
case 2:
|
||||
domElement.html("🔴 One other is watching");
|
||||
break;
|
||||
default:
|
||||
domElement.html("🔴 " + --number + " others are watching");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Activity capacity (when there is a guest limit)
|
||||
attendees_names = document.querySelector('#activity-' + data.activity + ' .stratt');
|
||||
if(attendees_names){
|
||||
html = "";
|
||||
const activitySocket = new WebSocket(
|
||||
'ws://'
|
||||
+ window.location.host
|
||||
+ '/ws/event/'
|
||||
+ eventId
|
||||
+ '/'
|
||||
);
|
||||
|
||||
for(index in data.guests){
|
||||
html += "<span class='badge bg-blue'>" + data.guests[index].join(", ") + "</span>";
|
||||
var globalLiveConnected = 0;
|
||||
|
||||
$("body").on("htmx:load.activities", function(evt) {
|
||||
console.info("HTMX OnLoad ...");
|
||||
var domElement = $(evt.detail.elt.querySelector(".live"));
|
||||
showLive(globalLiveConnected, domElement);
|
||||
});
|
||||
|
||||
activitySocket.onmessage = function(e) {
|
||||
var data = JSON.parse(e.data);
|
||||
|
||||
if(data.connected){
|
||||
globalLiveConnected = data.connected;
|
||||
showLive(globalLiveConnected);
|
||||
}
|
||||
|
||||
attendees_names.innerHTML = html;
|
||||
}
|
||||
// We caused the change, ignore
|
||||
if(rsvpId && data.rsvp && rsvpId == data.rsvp){
|
||||
return;
|
||||
}
|
||||
|
||||
// Activity attendees (when everyone can join)
|
||||
const attendees = $('#activity-' + data.activity + ' .numatt');
|
||||
if(attendees !== null && data.attendees !== null){
|
||||
attendees.html(data.attendees);
|
||||
}
|
||||
|
||||
// Activity capacity (when there is a guest limit)
|
||||
const capacity = $('#activity-' + data.activity + ' .numcap');
|
||||
if(capacity !== null && data.capacity !== null){
|
||||
capacity.html(data.capacity);
|
||||
}
|
||||
|
||||
// Activity capacity (when there is a guest limit)
|
||||
const activity = $('#activity-' + data.activity);
|
||||
if(activity !== null && data.capacity !== null){
|
||||
if(!(activity.hasClass("attending"))){
|
||||
if(data.capacity >= guestCount){
|
||||
activity.removeClass("bg-gray bd-gray");
|
||||
}
|
||||
else{
|
||||
activity.addClass("bg-gray bd-gray");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Activity capacity (when there is a guest limit)
|
||||
const attendees_names = $('#activity-' + data.activity + ' .stratt');
|
||||
if(attendees_names !== null){
|
||||
html = "";
|
||||
|
||||
for(index in data.guests){
|
||||
html += "<span class='badge bg-blue'>" + data.guests[index].join(", ") + "</span>";
|
||||
}
|
||||
|
||||
attendees_names.html(html);
|
||||
}
|
||||
};
|
||||
|
||||
activitySocket.onopen = function(e) {
|
||||
console.info('Activity socket opened');
|
||||
};
|
||||
|
||||
activitySocket.onclose = function(e) {
|
||||
console.error('Activity socket closed unexpectedly');
|
||||
};
|
||||
|
||||
activitySocket.onerror = function(e) {
|
||||
console.error('Activity socket had unexpected error');
|
||||
};
|
||||
|
||||
return (() => {
|
||||
$("body").off("htmx:load.activities");
|
||||
activitySocket.close();
|
||||
});
|
||||
};
|
||||
|
||||
activitySocket.onclose = function(e) {
|
||||
console.error('Activity socket closed unexpectedly');
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
<script src="{% static 'js/htmx.2.0.8.min.js' %}" integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz"></script>
|
||||
<script src="{% static 'js/htmx-ext-ws.2.0.4.min.js' %}" integrity="sha384-1RwI/nvUSrMRuNj7hX1+27J8XDdCoSLf0EjEyF69nacuWyiJYoQ/j39RT1mSnd2G"></script>
|
||||
<script src="{% static 'js/jquery-4.0.0.slim.min.js' %}"></script>
|
||||
<script src="{% static 'js/alpine.3.15.8.min.js' %}" defer></script>
|
||||
<link rel="stylesheet" href="{% static 'css/simple.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/styles.css' %}">
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
{% load static %}
|
||||
<script>
|
||||
const eventId = "{{ event.id }}";
|
||||
const rsvpId = "{{ rsvp.id }}";
|
||||
</script>
|
||||
<script src="{% static 'js/rsvp.js' %}"></script>
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,6 @@
|
||||
<div class="card" id="rsvp-activity">
|
||||
<h3>What do you want to go to?</h3>
|
||||
<h3>What do you want to go to? <span class="badge bg-red live">Connecting...</span> </h3>
|
||||
|
||||
|
||||
{% for group, activities in groups.items %}
|
||||
<h4 class="group-title">
|
||||
@@ -22,4 +23,8 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var guestCount = {{ rsvp.guest_count }};
|
||||
</script>
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="subcard cursor {% if activity.id in selected_activities %}bg-green{% elif activity.remaining_capacity < rsvp.guest_count %}bg-gray bd-gray{% endif %}"
|
||||
<div class="subcard cursor {% if activity.id in selected_activities %}bg-green attending{% elif activity.remaining_capacity < rsvp.guest_count %}bg-gray bd-gray{% endif %}"
|
||||
hx-post="{% url 'events:rsvp-post-activity' rsvp.id activity.id %}"
|
||||
hx-target="#rsvp-activity"
|
||||
hx-swap="outerHTML"
|
||||
@@ -34,8 +34,4 @@
|
||||
</span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
guestCount = {{ rsvp.guest_count }};
|
||||
</script>
|
||||
</div>
|
||||
Reference in New Issue
Block a user