mirror of
https://github.com/barkeser2002/offline-db.git
synced 2026-09-25 01:40:07 +03:00
Added `focus-visible:ring-2 focus-visible:ring-brand` classes to the episode links in `templates/home.html` and the footer links in `templates/base.html`. This ensures users navigating with a keyboard have clear, accessible focus states, resolving the issue noted in the Palette journal without affecting mouse users. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
363 lines
18 KiB
HTML
363 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" class="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}AniScrap{% endblock %}</title>
|
|
{% block meta %}{% endblock %}
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
darkMode: 'class',
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
brand: '#ff0055',
|
|
darkbg: '#0a0a0f',
|
|
glass: 'rgba(255, 255, 255, 0.05)'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
</head>
|
|
<body class="bg-darkbg text-white flex flex-col min-h-screen">
|
|
<nav class="p-4 border-b border-gray-800 bg-opacity-80 backdrop-blur-md sticky top-0 z-50">
|
|
<div class="container mx-auto flex justify-between items-center">
|
|
<a href="/" class="text-2xl font-bold text-brand tracking-widest">ANISCRAP</a>
|
|
|
|
<!-- Search Bar -->
|
|
<form role="search" aria-label="Site Search" action="{% url 'search' %}" method="get" class="hidden md:flex items-center bg-gray-800 rounded-lg px-3 py-1 border border-gray-700 focus-within:border-brand">
|
|
<input type="text" name="q" aria-label="Search query" placeholder="Search..." class="bg-transparent text-sm text-white focus:outline-none w-48">
|
|
<button type="submit" aria-label="Submit search" class="text-gray-400 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-brand rounded">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
</svg>
|
|
</button>
|
|
</form>
|
|
|
|
<div class="flex items-center space-x-4">
|
|
{% if user.is_authenticated %}
|
|
<!-- Notification Bell -->
|
|
<div class="relative" id="notification-container">
|
|
<button id="notification-btn" aria-label="Notifications" aria-haspopup="true" aria-expanded="false" class="relative p-2 text-gray-300 hover:text-white transition rounded-full focus:outline-none focus-visible:ring-2 focus-visible:ring-brand">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
|
</svg>
|
|
<!-- Badge -->
|
|
<span id="notification-badge" class="absolute top-0 right-0 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-white transform translate-x-1/4 -translate-y-1/4 bg-brand rounded-full hidden">
|
|
0
|
|
</span>
|
|
</button>
|
|
|
|
<!-- Dropdown -->
|
|
<div id="notification-dropdown" class="absolute right-0 mt-2 w-80 bg-gray-900 border border-gray-700 rounded-lg shadow-xl overflow-hidden z-50 hidden transition-all transform origin-top-right">
|
|
<div class="p-3 border-b border-gray-700 flex justify-between items-center bg-gray-800">
|
|
<h3 class="text-sm font-semibold text-white">Notifications</h3>
|
|
<button id="mark-all-read" class="text-xs text-brand hover:text-white transition rounded px-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand">Mark all read</button>
|
|
</div>
|
|
<div id="notification-list" class="max-h-64 overflow-y-auto">
|
|
<!-- Items will be injected here -->
|
|
<div class="p-4 text-center text-gray-500 text-sm">Loading...</div>
|
|
</div>
|
|
<div class="p-2 border-t border-gray-700 text-center bg-gray-800">
|
|
<a href="#" class="text-xs text-gray-400 hover:text-white transition focus:outline-none focus-visible:ring-2 focus-visible:ring-brand rounded px-1">View All</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<a href="{% url 'profile' %}" class="text-gray-300 hover:text-white transition font-semibold rounded px-2 py-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand">Welcome, {{ user.username }}</a>
|
|
{% else %}
|
|
<a href="/admin/" class="text-gray-300 hover:text-white transition rounded px-2 py-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand">Login</a>
|
|
{% endif %}
|
|
<button aria-label="Switch language" class="px-3 py-1 bg-gray-800 border border-gray-700 rounded hover:border-brand transition text-xs focus:outline-none focus-visible:ring-2 focus-visible:ring-brand">TR/EN</button>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
{% csrf_token %}
|
|
|
|
<main class="container mx-auto flex-grow p-6">
|
|
{% load ad_tags %}
|
|
<!-- Header Ad -->
|
|
<div class="mb-6 flex justify-center">
|
|
{% get_ad 'header' %}
|
|
</div>
|
|
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
<!-- Toast Container -->
|
|
<div id="toast-container" class="fixed bottom-4 right-4 z-50 flex flex-col items-end space-y-2 pointer-events-none">
|
|
<style>
|
|
#toast-container > div { pointer-events: auto; }
|
|
</style>
|
|
</div>
|
|
|
|
<footer class="bg-black py-8 border-t border-gray-900 mt-12">
|
|
<div class="container mx-auto text-center text-gray-500">
|
|
<p class="mb-2">© 2026 <strong>AniScrap</strong>. All rights reserved.</p>
|
|
<p class="text-sm mb-4">Developer: <strong class="text-brand">Barış Keser</strong></p>
|
|
<div class="flex justify-center space-x-6 text-sm">
|
|
<a href="mailto:info@bariskeser.com" class="hover:text-white transition focus:outline-none focus-visible:ring-2 focus-visible:ring-brand rounded px-1">Contact</a>
|
|
<a href="mailto:abuse@bariskeser.com" class="hover:text-white transition focus:outline-none focus-visible:ring-2 focus-visible:ring-brand rounded px-1">Report Abuse</a>
|
|
<a href="mailto:dmca@bariskeser.com" class="hover:text-white transition focus:outline-none focus-visible:ring-2 focus-visible:ring-brand rounded px-1">DMCA</a>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Toast Function
|
|
window.showToast = function(data) {
|
|
const container = document.getElementById('toast-container');
|
|
const toast = document.createElement('div');
|
|
// Initial state: hidden (translate-x)
|
|
toast.className = 'bg-gray-800 border-l-4 border-brand text-white px-4 py-3 rounded shadow-2xl w-80 transform transition-all duration-300 translate-x-[120%] opacity-0';
|
|
|
|
const flexDiv = document.createElement('div');
|
|
flexDiv.className = 'flex justify-between items-start';
|
|
|
|
const textDiv = document.createElement('div');
|
|
|
|
const title = document.createElement('h4');
|
|
title.className = 'font-bold text-sm text-brand mb-1';
|
|
title.textContent = data.title;
|
|
|
|
const message = document.createElement('p');
|
|
message.className = 'text-xs text-gray-300';
|
|
message.textContent = data.message;
|
|
|
|
textDiv.appendChild(title);
|
|
textDiv.appendChild(message);
|
|
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.className = 'text-gray-500 hover:text-white ml-2 focus:outline-none';
|
|
closeBtn.setAttribute('aria-label', 'Close notification');
|
|
closeBtn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>';
|
|
|
|
flexDiv.appendChild(textDiv);
|
|
flexDiv.appendChild(closeBtn);
|
|
toast.appendChild(flexDiv);
|
|
|
|
// Click to navigate
|
|
toast.addEventListener('click', (e) => {
|
|
// If close button clicked, don't navigate
|
|
if (e.target.closest('button')) {
|
|
closeToast(toast);
|
|
return;
|
|
}
|
|
if (data.link) window.location.href = data.link;
|
|
});
|
|
|
|
container.appendChild(toast);
|
|
|
|
// Animate In
|
|
requestAnimationFrame(() => {
|
|
toast.classList.remove('translate-x-[120%]', 'opacity-0');
|
|
});
|
|
|
|
// Auto Dismiss
|
|
setTimeout(() => {
|
|
closeToast(toast);
|
|
}, 5000);
|
|
}
|
|
|
|
function closeToast(toast) {
|
|
toast.classList.add('translate-x-[120%]', 'opacity-0');
|
|
setTimeout(() => {
|
|
if(toast.parentElement) toast.remove();
|
|
}, 300);
|
|
}
|
|
|
|
const btn = document.getElementById('notification-btn');
|
|
const dropdown = document.getElementById('notification-dropdown');
|
|
const list = document.getElementById('notification-list');
|
|
const badge = document.getElementById('notification-badge');
|
|
const markAllBtn = document.getElementById('mark-all-read');
|
|
|
|
if (!btn) return; // Stop if not authenticated/elements missing
|
|
|
|
// Get CSRF Token
|
|
function getCookie(name) {
|
|
let cookieValue = null;
|
|
if (document.cookie && document.cookie !== '') {
|
|
const cookies = document.cookie.split(';');
|
|
for (let i = 0; i < cookies.length; i++) {
|
|
const cookie = cookies[i].trim();
|
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return cookieValue;
|
|
}
|
|
const csrftoken = getCookie('csrftoken') || document.querySelector('[name=csrfmiddlewaretoken]').value;
|
|
|
|
// Toggle Dropdown
|
|
btn.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
dropdown.classList.toggle('hidden');
|
|
const isExpanded = !dropdown.classList.contains('hidden');
|
|
btn.setAttribute('aria-expanded', isExpanded);
|
|
if (isExpanded) {
|
|
fetchNotifications();
|
|
}
|
|
});
|
|
|
|
// Close on click outside
|
|
document.addEventListener('click', function(e) {
|
|
if (!dropdown.contains(e.target) && !btn.contains(e.target)) {
|
|
dropdown.classList.add('hidden');
|
|
btn.setAttribute('aria-expanded', 'false');
|
|
}
|
|
});
|
|
|
|
// Close on Escape key
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape' && !dropdown.classList.contains('hidden')) {
|
|
dropdown.classList.add('hidden');
|
|
btn.setAttribute('aria-expanded', 'false');
|
|
btn.focus(); // Return focus to button
|
|
}
|
|
});
|
|
|
|
// Poll for unread count
|
|
function pollUnread() {
|
|
fetch('/api/users/notifications/unread-count/')
|
|
.then(response => {
|
|
if(response.status === 401 || response.status === 403) return null;
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (data && data.count > 0) {
|
|
badge.textContent = data.count > 99 ? '99+' : data.count;
|
|
badge.classList.remove('hidden');
|
|
} else {
|
|
badge.classList.add('hidden');
|
|
}
|
|
})
|
|
.catch(err => console.error('Poll Error:', err));
|
|
}
|
|
|
|
// Fetch Notification List
|
|
function fetchNotifications() {
|
|
list.innerHTML = '<div class="p-4 text-center text-gray-500 text-sm">Loading...</div>';
|
|
fetch('/api/users/notifications/')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
list.innerHTML = '';
|
|
if (data.results && data.results.length > 0) {
|
|
data.results.forEach(notif => {
|
|
const item = document.createElement('div');
|
|
item.className = `p-3 border-b border-gray-700 hover:bg-gray-800 transition cursor-pointer ${notif.is_read ? 'opacity-60' : 'bg-gray-800 bg-opacity-50'}`;
|
|
|
|
// Create elements safely to avoid XSS
|
|
const title = document.createElement('h4');
|
|
title.className = 'text-sm font-bold text-white mb-1';
|
|
title.textContent = notif.title;
|
|
|
|
const msg = document.createElement('p');
|
|
msg.className = 'text-xs text-gray-400';
|
|
msg.textContent = notif.message;
|
|
|
|
const dateSpan = document.createElement('span');
|
|
dateSpan.className = 'text-xs text-gray-600 block mt-1';
|
|
dateSpan.textContent = new Date(notif.created_at).toLocaleDateString();
|
|
|
|
item.appendChild(title);
|
|
item.appendChild(msg);
|
|
item.appendChild(dateSpan);
|
|
|
|
item.addEventListener('click', () => {
|
|
if(!notif.is_read) markRead(notif.id);
|
|
if(notif.link) window.location.href = notif.link;
|
|
});
|
|
list.appendChild(item);
|
|
});
|
|
} else {
|
|
list.innerHTML = '<div class="p-4 text-center text-gray-500 text-sm">No notifications</div>';
|
|
}
|
|
})
|
|
.catch(err => {
|
|
list.innerHTML = '<div class="p-4 text-center text-red-500 text-sm">Error loading data</div>';
|
|
});
|
|
}
|
|
|
|
// Mark single as read
|
|
function markRead(id) {
|
|
fetch(`/api/users/notifications/${id}/read/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': csrftoken,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
}).then(() => {
|
|
pollUnread(); // Update badge
|
|
});
|
|
}
|
|
|
|
// Mark all as read
|
|
markAllBtn.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
fetch('/api/users/notifications/read-all/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': csrftoken,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
}).then(() => {
|
|
pollUnread();
|
|
fetchNotifications();
|
|
});
|
|
});
|
|
|
|
|
|
// Real-time & Polling Logic
|
|
{% if user.is_authenticated %}
|
|
pollUnread();
|
|
|
|
// Backup Polling (Every 60s)
|
|
setInterval(pollUnread, 60000);
|
|
|
|
// WebSocket Connection
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const wsUrl = protocol + '//' + window.location.host + '/ws/notifications/';
|
|
|
|
function connectWs() {
|
|
const notifSocket = new WebSocket(wsUrl);
|
|
|
|
notifSocket.onmessage = function(e) {
|
|
const data = JSON.parse(e.data);
|
|
if (data.type === 'notification') {
|
|
// Update Badge
|
|
let count = parseInt(badge.textContent) || 0;
|
|
if (badge.classList.contains('hidden')) count = 0;
|
|
count++;
|
|
badge.textContent = count > 99 ? '99+' : count;
|
|
badge.classList.remove('hidden');
|
|
|
|
// Show Toast
|
|
showToast(data);
|
|
|
|
// Refresh list if dropdown is open
|
|
if (!document.getElementById('notification-dropdown').classList.contains('hidden')) {
|
|
fetchNotifications();
|
|
}
|
|
}
|
|
};
|
|
|
|
notifSocket.onclose = function(e) {
|
|
console.error('Notification socket closed. Reconnecting in 5s...');
|
|
setTimeout(connectWs, 5000);
|
|
};
|
|
}
|
|
|
|
connectWs();
|
|
{% endif %}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|