RaktaPulse

This commit is contained in:
Flatlogic Bot 2026-02-18 09:21:22 +00:00
parent dba5db4b7d
commit c0bb59aeba
20 changed files with 639 additions and 10 deletions

View File

@ -86,6 +86,7 @@ TEMPLATES = [
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
# IMPORTANT: do not remove injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp # IMPORTANT: do not remove injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp
'core.context_processors.project_context', 'core.context_processors.project_context',
'core.context_processors.unread_messages',
], ],
}, },
}, },

View File

@ -1,5 +1,13 @@
import os import os
import time 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): def project_context(request):
""" """

View 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)),
],
),
]

View File

@ -148,3 +148,13 @@ class Notification(models.Model):
def __str__(self): def __str__(self):
return f"Notification for {self.user.username}: {self.message[:20]}..." 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}"

View File

@ -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 == '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 == '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_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 == '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-building-heart"></i> {% trans "Hospitals" %}</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 == '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> <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 %} {% if user.is_authenticated %}
@ -343,6 +343,16 @@
{{ user.notifications.count }} {{ user.notifications.count }}
</span> </span>
</a> </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 --> <!-- Profile Dropdown -->
<div class="dropdown"> <div class="dropdown">

View File

@ -52,6 +52,11 @@
<div class="d-flex justify-content-between align-items-center"> <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> <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"> <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="{% 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> <a href="tel:{{ req.contact_number }}" class="btn btn-sm btn-danger px-3 rounded-pill">Call</a>
</div> </div>

View 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 %}

View File

@ -79,7 +79,17 @@
</span> </span>
<p class="mb-0 text-muted extra-small" style="font-size: 0.7rem;">Phone: {{ donor.phone }}</p> <p class="mb-0 text-muted extra-small" style="font-size: 0.7rem;">Phone: {{ donor.phone }}</p>
</div> </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>
</div> </div>
{% endfor %} {% endfor %}

View File

@ -50,7 +50,7 @@
</p> </p>
{% endif %} {% 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> </div>
</div> </div>

View 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 %}

View File

@ -257,7 +257,14 @@
</span> </span>
<p class="mb-0 text-muted extra-small" style="font-size: 0.7rem;">Last Donated: {{ donor.last_donation_date|default:"Never" }}</p> <p class="mb-0 text-muted extra-small" style="font-size: 0.7rem;">Last Donated: {{ donor.last_donation_date|default:"Never" }}</p>
</div> </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>
</div> </div>
{% empty %} {% empty %}
@ -312,7 +319,14 @@
<p class="text-secondary extra-small mb-2"><i class="bi bi-hospital me-1"></i> {{ req.hospital }}</p> <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"> <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> <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>
</div> </div>
{% empty %} {% empty %}

View 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 %}

View File

@ -6,7 +6,7 @@ from .views import (
vaccination_dashboard, add_vaccination, live_map, vaccination_dashboard, add_vaccination, live_map,
request_blood, profile, volunteer_for_request, request_blood, profile, volunteer_for_request,
complete_donation, notifications_view, complete_donation, notifications_view,
register_donor, hospital_list register_donor, hospital_list, public_profile, inbox, chat
) )
urlpatterns = [ urlpatterns = [
@ -15,6 +15,9 @@ urlpatterns = [
path("logout/", logout_view, name="logout"), path("logout/", logout_view, name="logout"),
path("register/", register_view, name="register"), path("register/", register_view, name="register"),
path("profile/", profile, name="profile"), 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("donors/", donor_list, name="donor_list"),
path("requests/", blood_request_list, name="blood_request_list"), path("requests/", blood_request_list, name="blood_request_list"),
path("banks/", blood_bank_list, name="blood_bank_list"), path("banks/", blood_bank_list, name="blood_bank_list"),

View File

@ -1,14 +1,16 @@
import os import os
import platform import platform
import math import math
from django.db import models from django.db.models import Q
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.contrib.auth import login, logout, authenticate from django.contrib.auth import login, logout, authenticate
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
from django.utils import timezone 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 from .forms import UserUpdateForm, ProfileUpdateForm, UserRegisterForm
def hospital_list(request): def hospital_list(request):
@ -171,7 +173,7 @@ def home(request):
if request.user.is_authenticated: if request.user.is_authenticated:
# Get active involvements (where user is donor or requester) # Get active involvements (where user is donor or requester)
involved_events = DonationEvent.objects.filter( 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 is_completed=False
) )
context["involved_events"] = involved_events context["involved_events"] = involved_events
@ -434,3 +436,63 @@ def register_donor(request):
messages.error(request, "Please fill in all required fields.") 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]}) 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})