RaktaPulse
This commit is contained in:
parent
dba5db4b7d
commit
c0bb59aeba
Binary file not shown.
@ -86,6 +86,7 @@ TEMPLATES = [
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
# IMPORTANT: do not remove – injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp
|
||||
'core.context_processors.project_context',
|
||||
'core.context_processors.unread_messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,5 +1,13 @@
|
||||
import os
|
||||
import time
|
||||
from .models import Message
|
||||
|
||||
def unread_messages(request):
|
||||
if request.user.is_authenticated:
|
||||
return {
|
||||
'unread_messages_count': Message.objects.filter(receiver=request.user, is_read=False).count()
|
||||
}
|
||||
return {'unread_messages_count': 0}
|
||||
|
||||
def project_context(request):
|
||||
"""
|
||||
|
||||
27
core/migrations/0014_message.py
Normal file
27
core/migrations/0014_message.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-18 07:37
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0013_userprofile_profile_pic'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Message',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('content', models.TextField()),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('is_read', models.BooleanField(default=False)),
|
||||
('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL)),
|
||||
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0014_message.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0014_message.cpython-311.pyc
Normal file
Binary file not shown.
@ -148,3 +148,13 @@ class Notification(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return f"Notification for {self.user.username}: {self.message[:20]}..."
|
||||
|
||||
class Message(models.Model):
|
||||
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_messages')
|
||||
receiver = models.ForeignKey(User, on_delete=models.CASCADE, related_name='received_messages')
|
||||
content = models.TextField()
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
is_read = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return f"From {self.sender.username} to {self.receiver.username} at {self.timestamp}"
|
||||
|
||||
@ -275,8 +275,8 @@
|
||||
<li class="{% if request.resolver_match.url_name == 'home' %}active{% endif %}"><a href="{% url 'home' %}"><i class="bi bi-grid-1x2-fill"></i> {% trans "Dashboard" %}</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'donor_list' %}active{% endif %}"><a href="{% url 'donor_list' %}"><i class="bi bi-people-fill"></i> {% trans "Donors" %}</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'blood_request_list' %}active{% endif %}"><a href="{% url 'blood_request_list' %}"><i class="bi bi-megaphone-fill"></i> {% trans "Blood Requests" %}</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'blood_bank_list' %}active{% endif %}"><a href="{% url 'blood_bank_list' %}"><i class="bi bi-hospital-fill"></i> {% trans "Blood Banks" %}</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'hospital_list' %}active{% endif %}"><a href="{% url 'hospital_list' %}"><i class="bi bi-building-heart"></i> {% trans "Hospitals" %}</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'blood_bank_list' %}active{% endif %}"><a href="{% url 'blood_bank_list' %}"><i class="bi bi-droplet-half"></i> {% trans "Blood Banks" %}</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'hospital_list' %}active{% endif %}"><a href="{% url 'hospital_list' %}"><i class="bi bi-hospital-fill"></i> {% trans "Hospitals" %}</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'live_map' %}active{% endif %}"><a href="{% url 'live_map' %}"><i class="bi bi-map text-danger"></i> {% trans "Live Alerts" %}</a></li>
|
||||
<li class="{% if request.resolver_match.url_name == 'vaccination_info' %}active{% endif %}"><a href="{% url 'vaccination_info' %}"><i class="bi bi-shield-check"></i> {% trans "Vaccination" %}</a></li>
|
||||
{% if user.is_authenticated %}
|
||||
@ -343,6 +343,16 @@
|
||||
{{ user.notifications.count }}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<!-- Messages Inbox -->
|
||||
<a href="{% url 'inbox' %}" class="text-decoration-none me-2 position-relative">
|
||||
<i class="bi bi-chat-dots-fill fs-5 text-secondary"></i>
|
||||
{% if unread_messages_count > 0 %}
|
||||
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="font-size: 0.6rem;">
|
||||
{{ unread_messages_count }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
|
||||
<!-- Profile Dropdown -->
|
||||
<div class="dropdown">
|
||||
|
||||
@ -52,6 +52,11 @@
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="small text-muted"><i class="bi bi-clock me-1"></i> {{ req.created_at|timesince }} ago</span>
|
||||
<div class="d-flex gap-2">
|
||||
{% if req.user %}
|
||||
<a href="{% url 'chat' req.user.username %}" class="btn btn-sm btn-outline-secondary px-2 rounded-pill" title="Message Requester">
|
||||
<i class="bi bi-chat-dots-fill"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'volunteer_for_request' req.id %}" class="btn btn-sm btn-outline-danger px-3 rounded-pill">Volunteer</a>
|
||||
<a href="tel:{{ req.contact_number }}" class="btn btn-sm btn-danger px-3 rounded-pill">Call</a>
|
||||
</div>
|
||||
|
||||
353
core/templates/core/chat.html
Normal file
353
core/templates/core/chat.html
Normal file
@ -0,0 +1,353 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Chat with {{ other_user.username }} - RaktaPulse{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
.chat-container {
|
||||
height: calc(100vh - 250px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.chat-messages {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
.message {
|
||||
max-width: 75%;
|
||||
padding: 12px 16px;
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.message-sent {
|
||||
align-self: flex-end;
|
||||
background-color: var(--pulse-red);
|
||||
color: white;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
.message-received {
|
||||
align-self: flex-start;
|
||||
background-color: white;
|
||||
color: var(--text-primary);
|
||||
border-bottom-left-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.message-time {
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.7;
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
#videoGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
background: #000;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.video-wrapper {
|
||||
position: relative;
|
||||
aspect-ratio: 4/3;
|
||||
background: #222;
|
||||
}
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.video-label {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
background: rgba(0,0,0,0.5);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.call-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
background: rgba(0,0,0,0.8);
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="glass-card chat-container">
|
||||
<div class="d-flex align-items-center gap-3 pb-3 mb-3 border-bottom">
|
||||
<a href="{% url 'inbox' %}" class="btn btn-link text-secondary p-0">
|
||||
<i class="bi bi-arrow-left fs-4"></i>
|
||||
</a>
|
||||
<div class="rounded-circle overflow-hidden border border-danger-subtle" style="width: 45px; height: 45px;">
|
||||
{% if other_user.profile.profile_pic %}
|
||||
<img src="{{ other_user.profile.profile_pic.url }}" alt="{{ other_user.username }}" class="w-100 h-100 object-fit-cover">
|
||||
{% else %}
|
||||
<div class="bg-danger bg-opacity-10 w-100 h-100 d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-person-fill text-danger"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="fw-bold mb-0">
|
||||
<a href="{% url 'public_profile' other_user.username %}" class="text-decoration-none text-dark">
|
||||
{{ other_user.first_name }} {{ other_user.last_name|default:other_user.username }}
|
||||
</a>
|
||||
</h6>
|
||||
<small class="text-success d-flex align-items-center gap-1">
|
||||
<span class="rounded-circle bg-success" style="width: 8px; height: 8px;"></span> Online
|
||||
</small>
|
||||
</div>
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
<button id="startAudioCall" class="btn btn-outline-secondary rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;" title="Audio Call">
|
||||
<i class="bi bi-telephone-fill"></i>
|
||||
</button>
|
||||
<button id="startVideoCall" class="btn btn-outline-danger rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;" title="Video Call">
|
||||
<i class="bi bi-camera-video-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-messages" id="chatMessages">
|
||||
{% for msg in chat_messages %}
|
||||
<div class="message {% if msg.sender == user %}message-sent{% else %}message-received{% endif %}">
|
||||
{{ msg.content }}
|
||||
<span class="message-time {% if msg.sender == user %}text-white-50{% else %}text-secondary{% endif %}">
|
||||
{{ msg.timestamp|date:"g:i a" }}
|
||||
</span>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-center my-auto text-secondary">
|
||||
<p class="mb-0">No messages yet. Say hi!</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<form method="post" class="d-flex gap-2">
|
||||
{% csrf_token %}
|
||||
<input type="text" name="content" class="form-control rounded-pill px-4" placeholder="Type your message..." autocomplete="off" required>
|
||||
<button type="submit" class="btn btn-danger rounded-circle d-flex align-items-center justify-content-center" style="width: 45px; height: 45px;">
|
||||
<i class="bi bi-send-fill"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Call Modal -->
|
||||
<div class="modal fade" id="callModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content bg-dark border-0 overflow-hidden rounded-4">
|
||||
<div class="modal-body p-0">
|
||||
<div id="videoGrid">
|
||||
<div class="video-wrapper" id="localVideoWrapper">
|
||||
<video id="localVideo" autoplay muted playsinline></video>
|
||||
<div class="video-label">{% trans "You" %}</div>
|
||||
</div>
|
||||
<div class="video-wrapper" id="remoteVideoWrapper" style="display: none;">
|
||||
<video id="remoteVideo" autoplay playsinline></video>
|
||||
<div class="video-label" id="remoteLabel">{{ other_user.username }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="call-controls">
|
||||
<button id="toggleMic" class="btn btn-outline-light rounded-circle d-flex align-items-center justify-content-center" style="width: 45px; height: 45px;"><i class="bi bi-mic-fill"></i></button>
|
||||
<button id="toggleVideo" class="btn btn-outline-light rounded-circle d-flex align-items-center justify-content-center" style="width: 45px; height: 45px;"><i class="bi bi-camera-video-fill"></i></button>
|
||||
<button id="endCall" class="btn btn-danger rounded-pill px-4 fw-bold">{% trans "End Call" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Incoming Call UI -->
|
||||
<div id="incomingCallUI" class="position-fixed top-0 start-50 translate-middle-x mt-4 glass-card p-3 shadow-lg border-danger animate__animated animate__fadeInDown" style="display: none; z-index: 9999; min-width: 320px; border: 2px solid var(--pulse-red) !important;">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="bg-danger bg-opacity-10 p-2 rounded-circle">
|
||||
<i class="bi bi-telephone-inbound-fill text-danger fs-4"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-0 fw-bold">{% trans "Incoming Call" %}</h6>
|
||||
<small class="text-secondary">{{ other_user.username }} {% trans "is calling..." %}</small>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button id="rejectCall" class="btn btn-light rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;"><i class="bi bi-x-lg"></i></button>
|
||||
<button id="acceptCall" class="btn btn-danger rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;"><i class="bi bi-check-lg"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
|
||||
<script>
|
||||
// Chat UI logic
|
||||
const chatMessages = document.getElementById('chatMessages');
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
|
||||
// Video/Audio Call Logic
|
||||
const MY_USERNAME = "{{ user.username }}";
|
||||
const OTHER_USERNAME = "{{ other_user.username }}";
|
||||
const PEER_ID_PREFIX = "raktapulse_";
|
||||
|
||||
// Sanitize username for PeerJS ID (only alphanumeric, -, _)
|
||||
const sanitizeId = (id) => id.replace(/[^a-zA-Z0-9-_]/g, '_');
|
||||
|
||||
// Initialize PeerJS
|
||||
let peer = new Peer(PEER_ID_PREFIX + sanitizeId(MY_USERNAME), {
|
||||
debug: 1
|
||||
});
|
||||
|
||||
let localStream;
|
||||
let currentCall;
|
||||
|
||||
const callModal = new bootstrap.Modal(document.getElementById('callModal'));
|
||||
const incomingCallUI = document.getElementById('incomingCallUI');
|
||||
const localVideo = document.getElementById('localVideo');
|
||||
const remoteVideo = document.getElementById('remoteVideo');
|
||||
const remoteVideoWrapper = document.getElementById('remoteVideoWrapper');
|
||||
|
||||
// Handle Peer Open
|
||||
peer.on('open', (id) => {
|
||||
console.log('My peer ID is: ' + id);
|
||||
});
|
||||
|
||||
// Handle incoming calls
|
||||
peer.on('call', (call) => {
|
||||
console.log('Incoming call from: ' + call.peer);
|
||||
// Only accept if it's the other user we are chatting with
|
||||
if (call.peer === PEER_ID_PREFIX + sanitizeId(OTHER_USERNAME)) {
|
||||
currentCall = call;
|
||||
incomingCallUI.style.display = 'block';
|
||||
|
||||
// Auto-hide incoming call after 30 seconds if not answered
|
||||
setTimeout(() => {
|
||||
if (incomingCallUI.style.display === 'block') {
|
||||
incomingCallUI.style.display = 'none';
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
});
|
||||
|
||||
// Start Call Function
|
||||
async function startCall(videoEnabled = true) {
|
||||
try {
|
||||
localStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: videoEnabled,
|
||||
audio: true
|
||||
});
|
||||
|
||||
localVideo.srcObject = localStream;
|
||||
callModal.show();
|
||||
|
||||
const call = peer.call(PEER_ID_PREFIX + sanitizeId(OTHER_USERNAME), localStream);
|
||||
handleCall(call);
|
||||
} catch (err) {
|
||||
console.error('Failed to get local stream', err);
|
||||
alert('Could not access camera or microphone. Please ensure you have given permission.');
|
||||
}
|
||||
}
|
||||
|
||||
function handleCall(call) {
|
||||
currentCall = call;
|
||||
call.on('stream', (remoteStream) => {
|
||||
console.log('Received remote stream');
|
||||
remoteVideo.srcObject = remoteStream;
|
||||
remoteVideoWrapper.style.display = 'block';
|
||||
});
|
||||
call.on('close', () => {
|
||||
endCall();
|
||||
});
|
||||
call.on('error', (err) => {
|
||||
console.error('Call error:', err);
|
||||
endCall();
|
||||
});
|
||||
}
|
||||
|
||||
// Button Listeners
|
||||
document.getElementById('startVideoCall').addEventListener('click', () => startCall(true));
|
||||
document.getElementById('startAudioCall').addEventListener('click', () => startCall(false));
|
||||
|
||||
document.getElementById('acceptCall').addEventListener('click', async () => {
|
||||
incomingCallUI.style.display = 'none';
|
||||
try {
|
||||
localStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
audio: true
|
||||
});
|
||||
localVideo.srcObject = localStream;
|
||||
callModal.show();
|
||||
currentCall.answer(localStream);
|
||||
handleCall(currentCall);
|
||||
} catch (err) {
|
||||
console.error('Failed to get local stream', err);
|
||||
currentCall.close();
|
||||
alert('Could not access camera or microphone.');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('rejectCall').addEventListener('click', () => {
|
||||
incomingCallUI.style.display = 'none';
|
||||
if (currentCall) currentCall.close();
|
||||
});
|
||||
|
||||
document.getElementById('endCall').addEventListener('click', () => {
|
||||
endCall();
|
||||
});
|
||||
|
||||
function endCall() {
|
||||
if (currentCall) currentCall.close();
|
||||
if (localStream) {
|
||||
localStream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
callModal.hide();
|
||||
remoteVideoWrapper.style.display = 'none';
|
||||
localVideo.srcObject = null;
|
||||
remoteVideo.srcObject = null;
|
||||
}
|
||||
|
||||
// Toggle Mic/Video
|
||||
document.getElementById('toggleMic').addEventListener('click', function() {
|
||||
if (!localStream) return;
|
||||
const audioTrack = localStream.getAudioTracks()[0];
|
||||
if (audioTrack) {
|
||||
audioTrack.enabled = !audioTrack.enabled;
|
||||
this.innerHTML = audioTrack.enabled ? '<i class="bi bi-mic-fill"></i>' : '<i class="bi bi-mic-mute-fill"></i>';
|
||||
this.classList.toggle('btn-outline-light');
|
||||
this.classList.toggle('btn-danger');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('toggleVideo').addEventListener('click', function() {
|
||||
if (!localStream) return;
|
||||
const videoTrack = localStream.getVideoTracks()[0];
|
||||
if (videoTrack) {
|
||||
videoTrack.enabled = !videoTrack.enabled;
|
||||
this.innerHTML = videoTrack.enabled ? '<i class="bi bi-camera-video-fill"></i>' : '<i class="bi bi-camera-video-off-fill"></i>';
|
||||
this.classList.toggle('btn-outline-light');
|
||||
this.classList.toggle('btn-danger');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle cleanup on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
endCall();
|
||||
peer.destroy();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -79,7 +79,17 @@
|
||||
</span>
|
||||
<p class="mb-0 text-muted extra-small" style="font-size: 0.7rem;">Phone: {{ donor.phone }}</p>
|
||||
</div>
|
||||
<a href="tel:{{ donor.phone }}" class="btn btn-outline-danger btn-sm px-3 rounded-pill">Contact Now</a>
|
||||
<div class="d-flex gap-2">
|
||||
{% if donor.user %}
|
||||
<a href="{% url 'public_profile' donor.user.username %}" class="btn btn-outline-secondary btn-sm rounded-pill" title="View Profile">
|
||||
<i class="bi bi-person-circle"></i>
|
||||
</a>
|
||||
<a href="{% url 'chat' donor.user.username %}" class="btn btn-outline-danger btn-sm rounded-pill" title="Send Message">
|
||||
<i class="bi bi-chat-dots-fill"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="tel:{{ donor.phone }}" class="btn btn-danger btn-sm px-3 rounded-pill">Call</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<a href="{% url 'request_blood' %}?hospital={{ hospital.name|urlencode }}" class="btn btn-outline-danger btn-sm w-100 rounded-pill">Request Blood Here</a>
|
||||
<a href="{% url 'request_blood' %}?hospital={{ hospital.name|urlencode }}" class="btn btn-outline-danger btn-sm w-100 rounded-pill"><i class="bi bi-plus-circle me-1"></i> Request Blood Here</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
54
core/templates/core/inbox.html
Normal file
54
core/templates/core/inbox.html
Normal file
@ -0,0 +1,54 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Inbox - RaktaPulse{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold mb-0">Messages</h2>
|
||||
</div>
|
||||
|
||||
<div class="glass-card p-0 overflow-hidden">
|
||||
{% if conversations %}
|
||||
<div class="list-group list-group-flush">
|
||||
{% for conv in conversations %}
|
||||
<a href="{% url 'chat' conv.user.username %}" class="list-group-item list-group-item-action p-4 border-0 border-bottom d-flex align-items-center gap-3">
|
||||
<div class="rounded-circle overflow-hidden border border-danger-subtle flex-shrink-0" style="width: 60px; height: 60px;">
|
||||
{% if conv.user.profile.profile_pic %}
|
||||
<img src="{{ conv.user.profile.profile_pic.url }}" alt="{{ conv.user.username }}" class="w-100 h-100 object-fit-cover">
|
||||
{% else %}
|
||||
<div class="bg-danger bg-opacity-10 w-100 h-100 d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-person-fill text-danger fs-4"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<h6 class="fw-bold mb-0 text-dark">{{ conv.user.first_name }} {{ conv.user.last_name|default:conv.user.username }}</h6>
|
||||
<small class="text-secondary">{{ conv.last_message.timestamp|date:"M d, g:i a" }}</small>
|
||||
</div>
|
||||
<p class="text-secondary mb-0 text-truncate {% if not conv.last_message.is_read and conv.last_message.receiver == user %}fw-bold text-dark{% endif %}">
|
||||
{% if conv.last_message.sender == user %}You: {% endif %}
|
||||
{{ conv.last_message.content }}
|
||||
</p>
|
||||
</div>
|
||||
{% if not conv.last_message.is_read and conv.last_message.receiver == user %}
|
||||
<div class="bg-danger rounded-circle" style="width: 10px; height: 10px;"></div>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-5 text-center">
|
||||
<div class="bg-light rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 80px; height: 80px;">
|
||||
<i class="bi bi-chat-dots text-secondary fs-1"></i>
|
||||
</div>
|
||||
<h5 class="text-dark">No messages yet</h5>
|
||||
<p class="text-secondary">Start a conversation with a donor or requester!</p>
|
||||
<a href="{% url 'donor_list' %}" class="btn btn-danger px-4 mt-2">Find Donors</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -257,7 +257,14 @@
|
||||
</span>
|
||||
<p class="mb-0 text-muted extra-small" style="font-size: 0.7rem;">Last Donated: {{ donor.last_donation_date|default:"Never" }}</p>
|
||||
</div>
|
||||
<a href="tel:{{ donor.phone }}" class="btn btn-outline-danger btn-sm px-3 rounded-pill">Contact</a>
|
||||
<div class="d-flex gap-2">
|
||||
{% if donor.user %}
|
||||
<a href="{% url 'chat' donor.user.username %}" class="btn btn-outline-danger btn-sm rounded-pill" title="Message">
|
||||
<i class="bi bi-chat-dots-fill"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="tel:{{ donor.phone }}" class="btn btn-danger btn-sm px-3 rounded-pill">Call</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
@ -312,7 +319,14 @@
|
||||
<p class="text-secondary extra-small mb-2"><i class="bi bi-hospital me-1"></i> {{ req.hospital }}</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted" style="font-size: 0.7rem;">{{ req.created_at|timesince }} ago</small>
|
||||
<a href="tel:{{ req.contact_number }}" class="btn btn-link text-danger p-0 text-decoration-none small">Help Now →</a>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
{% if req.user %}
|
||||
<a href="{% url 'chat' req.user.username %}" class="text-secondary" title="Message">
|
||||
<i class="bi bi-chat-dots-fill"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="tel:{{ req.contact_number }}" class="btn btn-link text-danger p-0 text-decoration-none small">Help Now →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
|
||||
72
core/templates/core/public_profile.html
Normal file
72
core/templates/core/public_profile.html
Normal file
@ -0,0 +1,72 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{{ profile_user.username }}'s Profile - RaktaPulse{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="glass-card">
|
||||
<div class="text-center mb-4">
|
||||
<div class="rounded-circle overflow-hidden border border-danger-subtle d-inline-block p-1 mb-3" style="width: 150px; height: 150px;">
|
||||
{% if profile.profile_pic %}
|
||||
<img src="{{ profile.profile_pic.url }}" alt="{{ profile_user.username }}" class="w-100 h-100 object-fit-cover rounded-circle">
|
||||
{% else %}
|
||||
<div class="bg-danger bg-opacity-10 w-100 h-100 d-flex align-items-center justify-content-center rounded-circle">
|
||||
<i class="bi bi-person-fill text-danger" style="font-size: 5rem;"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h2 class="fw-bold mb-0">{{ profile_user.first_name }} {{ profile_user.last_name }}</h2>
|
||||
<p class="text-secondary mb-3">@{{ profile_user.username }}</p>
|
||||
|
||||
{% if donor %}
|
||||
<div class="d-flex justify-content-center gap-2 mb-4">
|
||||
<span class="badge bg-danger fs-5 px-3">{{ donor.blood_group }}</span>
|
||||
{% if donor.is_available %}
|
||||
<span class="badge bg-success d-flex align-items-center"><i class="bi bi-check-circle-fill me-1"></i> Available</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary d-flex align-items-center"><i class="bi bi-clock-fill me-1"></i> Not Available</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-sm-6">
|
||||
<div class="p-3 bg-light rounded-3 h-100">
|
||||
<h6 class="text-secondary fw-bold small text-uppercase mb-2">Location</h6>
|
||||
<p class="mb-0"><i class="bi bi-geo-alt-fill text-danger me-2"></i>{{ profile.location|default:"Not specified" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="p-3 bg-light rounded-3 h-100">
|
||||
<h6 class="text-secondary fw-bold small text-uppercase mb-2">Member Since</h6>
|
||||
<p class="mb-0"><i class="bi bi-calendar-event-fill text-danger me-2"></i>{{ profile_user.date_joined|date:"F Y" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h5 class="fw-bold mb-3">About</h5>
|
||||
<div class="p-3 border rounded-3 bg-light-subtle">
|
||||
{{ profile.bio|default:"No bio provided."|linebreaks }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
{% if user.is_authenticated and user != profile_user %}
|
||||
<a href="{% url 'chat' profile_user.username %}" class="btn btn-danger py-2">
|
||||
<i class="bi bi-chat-dots-fill me-2"></i> Send Message
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'donor_list' %}" class="btn btn-outline-secondary py-2">
|
||||
<i class="bi bi-arrow-left me-2"></i> Back to Donors
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -6,7 +6,7 @@ from .views import (
|
||||
vaccination_dashboard, add_vaccination, live_map,
|
||||
request_blood, profile, volunteer_for_request,
|
||||
complete_donation, notifications_view,
|
||||
register_donor, hospital_list
|
||||
register_donor, hospital_list, public_profile, inbox, chat
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@ -15,6 +15,9 @@ urlpatterns = [
|
||||
path("logout/", logout_view, name="logout"),
|
||||
path("register/", register_view, name="register"),
|
||||
path("profile/", profile, name="profile"),
|
||||
path("profile/<str:username>/", public_profile, name="public_profile"),
|
||||
path("inbox/", inbox, name="inbox"),
|
||||
path("chat/<str:username>/", chat, name="chat"),
|
||||
path("donors/", donor_list, name="donor_list"),
|
||||
path("requests/", blood_request_list, name="blood_request_list"),
|
||||
path("banks/", blood_bank_list, name="blood_bank_list"),
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import os
|
||||
import platform
|
||||
import math
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib.auth import login, logout, authenticate
|
||||
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.utils import timezone
|
||||
from .models import Donor, BloodRequest, BloodBank, VaccineRecord, UserProfile, BLOOD_GROUPS, DonationEvent, Notification, Hospital
|
||||
from django.contrib.auth.models import User
|
||||
from .models import Donor, BloodRequest, BloodBank, VaccineRecord, UserProfile, BLOOD_GROUPS, DonationEvent, Notification, Hospital, Message
|
||||
|
||||
from .forms import UserUpdateForm, ProfileUpdateForm, UserRegisterForm
|
||||
|
||||
def hospital_list(request):
|
||||
@ -171,7 +173,7 @@ def home(request):
|
||||
if request.user.is_authenticated:
|
||||
# Get active involvements (where user is donor or requester)
|
||||
involved_events = DonationEvent.objects.filter(
|
||||
(models.Q(donor_user=request.user) | models.Q(request__user=request.user)),
|
||||
(Q(donor_user=request.user) | Q(request__user=request.user)),
|
||||
is_completed=False
|
||||
)
|
||||
context["involved_events"] = involved_events
|
||||
@ -434,3 +436,63 @@ def register_donor(request):
|
||||
messages.error(request, "Please fill in all required fields.")
|
||||
|
||||
return render(request, 'core/register_donor.html', {'blood_groups': [g[0] for g in BLOOD_GROUPS]})
|
||||
|
||||
def public_profile(request, username):
|
||||
user = User.objects.get(username=username)
|
||||
profile = user.profile
|
||||
donor_profile = getattr(user, 'donor_profile', None)
|
||||
|
||||
context = {
|
||||
'profile_user': user,
|
||||
'profile': profile,
|
||||
'donor': donor_profile,
|
||||
}
|
||||
return render(request, 'core/public_profile.html', context)
|
||||
|
||||
@login_required
|
||||
def inbox(request):
|
||||
# Get all users the current user has messaged or received messages from
|
||||
sent_to = Message.objects.filter(sender=request.user).values_list('receiver', flat=True)
|
||||
received_from = Message.objects.filter(receiver=request.user).values_list('sender', flat=True)
|
||||
user_ids = set(list(sent_to) + list(received_from))
|
||||
|
||||
users = User.objects.filter(id__in=user_ids)
|
||||
|
||||
# Get last message for each conversation
|
||||
conversations = []
|
||||
for user in users:
|
||||
last_message = Message.objects.filter(
|
||||
(Q(sender=request.user) & Q(receiver=user)) |
|
||||
(Q(sender=user) & Q(receiver=request.user))
|
||||
).order_by('-timestamp').first()
|
||||
conversations.append({
|
||||
'user': user,
|
||||
'last_message': last_message
|
||||
})
|
||||
|
||||
conversations.sort(key=lambda x: x['last_message'].timestamp, reverse=True)
|
||||
|
||||
return render(request, 'core/inbox.html', {'conversations': conversations})
|
||||
|
||||
@login_required
|
||||
def chat(request, username):
|
||||
other_user = User.objects.get(username=username)
|
||||
if request.method == "POST":
|
||||
content = request.POST.get('content')
|
||||
if content:
|
||||
Message.objects.create(
|
||||
sender=request.user,
|
||||
receiver=other_user,
|
||||
content=content
|
||||
)
|
||||
return redirect('chat', username=username)
|
||||
|
||||
messages = Message.objects.filter(
|
||||
(Q(sender=request.user) & Q(receiver=other_user)) |
|
||||
(Q(sender=other_user) & Q(receiver=request.user))
|
||||
).order_by('timestamp')
|
||||
|
||||
# Mark as read
|
||||
Message.objects.filter(sender=other_user, receiver=request.user, is_read=False).update(is_read=True)
|
||||
|
||||
return render(request, 'core/chat.html', {'other_user': other_user, 'chat_messages': messages})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user