Files
Barış Keserandgoogle-labs-jules[bot] 099ef897a8 🎨 Palette: Add focus-visible styles for keyboard accessibility (#182)
- Added `focus-visible:ring-2` to buttons, links, and inputs across templates.
- Added `focus-within` to anime cards.
- Added `aria-label` to chat inputs and send buttons.
- Documented learning in `.jules/palette.md`.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-02-28 20:12:30 +00:00

176 lines
8.8 KiB
HTML

{% extends 'base.html' %}
{% block title %}{{ anime.title }} | AniScrap{% endblock %}
{% block meta %}
<meta property="og:title" content="{{ anime.title }}">
<meta property="og:description" content="{{ anime.synopsis|truncatechars:200 }}">
{% if anime.cover_image %}
<meta property="og:image" content="{{ anime.cover_image }}">
{% endif %}
<meta property="og:type" content="video.tv_show">
{% endblock %}
{% block content %}
<div class="max-w-6xl mx-auto p-4">
<!-- Anime Header -->
<div class="bg-gray-800 rounded-xl overflow-hidden shadow-2xl mb-8">
<div class="md:flex">
<div class="md:w-1/3">
{% if anime.cover_image %}
<img src="{{ anime.cover_image }}" alt="{{ anime.title }}" class="w-full h-full object-cover">
{% else %}
<div class="w-full h-96 bg-gray-900 flex items-center justify-center text-gray-500">
No Cover Image
</div>
{% endif %}
</div>
<div class="md:w-2/3 p-8">
<div class="flex justify-between items-start mb-4">
<h1 class="text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-600">
{{ anime.title }}
</h1>
{% if user.is_authenticated %}
<button id="subscribe-btn"
class="inline-flex items-center justify-center gap-2 px-4 py-2 rounded-full font-bold transition transform hover:scale-105 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-800 focus-visible:ring-brand {% if is_subscribed %}bg-gray-700 text-gray-300 hover:bg-gray-600{% else %}bg-brand text-white hover:bg-brand-dark{% endif %}"
data-anime-id="{{ anime.id }}"
data-subscribed="{{ is_subscribed|yesno:'true,false' }}">
<svg id="subscribe-spinner" class="animate-spin h-5 w-5 text-current hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span id="subscribe-text">
{% if is_subscribed %}
Unsubscribe
{% else %}
Subscribe
{% endif %}
</span>
</button>
{% endif %}
</div>
<p class="text-gray-300 leading-relaxed mb-6 text-lg">
{{ anime.synopsis|default:"No synopsis available." }}
</p>
<div class="flex items-center space-x-4 text-sm text-gray-400">
<span>Added: {{ anime.created_at|date:"F j, Y" }}</span>
<span>•</span>
<span>{{ seasons|length }} Seasons</span>
</div>
</div>
</div>
</div>
<!-- Seasons and Episodes -->
<div class="space-y-8">
{% for season in seasons %}
<div class="bg-gray-800 rounded-xl p-6 shadow-lg border border-gray-700">
<h2 class="text-2xl font-bold text-white mb-4 border-b border-gray-700 pb-2">
{% if season.title %}{{ season.title }}{% else %}Season {{ season.number }}{% endif %}
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for episode in season.episodes.all %}
<a href="{% url 'watch' episode.id %}" class="block group rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-800 focus-visible:ring-brand">
<div class="bg-gray-700 rounded-lg overflow-hidden hover:bg-gray-600 transition duration-300">
<div class="relative h-40">
{% if episode.thumbnail %}
<img src="{{ episode.thumbnail }}" alt="{{ episode.title }}" class="w-full h-full object-cover group-hover:opacity-75 transition">
{% else %}
<div class="w-full h-full bg-gray-900 flex items-center justify-center text-gray-500">No Thumb</div>
{% endif %}
<div class="absolute bottom-2 right-2 bg-black bg-opacity-80 px-2 py-1 rounded text-xs text-white">
Ep {{ episode.number }}
</div>
</div>
<div class="p-3">
<h3 class="text-white font-semibold truncate group-hover:text-purple-400 transition">
{% if episode.title %}{{ episode.title }}{% else %}Episode {{ episode.number }}{% endif %}
</h3>
</div>
</div>
</a>
{% empty %}
<p class="text-gray-500 italic col-span-3">No episodes in this season yet.</p>
{% endfor %}
</div>
</div>
{% empty %}
<div class="text-center text-gray-500 py-12">
No seasons added yet.
</div>
{% endfor %}
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('subscribe-btn');
const spinner = document.getElementById('subscribe-spinner');
const text = document.getElementById('subscribe-text');
if (!btn) return;
btn.addEventListener('click', async () => {
const animeId = btn.getAttribute('data-anime-id');
const isSubscribed = btn.getAttribute('data-subscribed') === 'true';
// Optimistic UI Update
btn.disabled = true;
if (spinner) spinner.classList.remove('hidden');
try {
// Get CSRF Token from Cookie (Robust)
let csrftoken = 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, 11) === ('csrftoken=')) {
csrftoken = decodeURIComponent(cookie.substring(11));
break;
}
}
}
// Fallback to DOM if cookie is missing (e.g. HttpOnly, though CSRF shouldn't be)
if (!csrftoken) {
const input = document.querySelector('[name=csrfmiddlewaretoken]');
if (input) csrftoken = input.value;
}
const response = await fetch(`/api/v1/anime/${animeId}/subscribe/`, {
method: 'POST',
headers: {
'X-CSRFToken': csrftoken,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
if (data.status === 'subscribed') {
text.textContent = 'Unsubscribe';
btn.setAttribute('data-subscribed', 'true');
btn.classList.remove('bg-brand', 'text-white', 'hover:bg-brand-dark');
btn.classList.add('bg-gray-700', 'text-gray-300', 'hover:bg-gray-600');
} else {
text.textContent = 'Subscribe';
btn.setAttribute('data-subscribed', 'false');
btn.classList.remove('bg-gray-700', 'text-gray-300', 'hover:bg-gray-600');
btn.classList.add('bg-brand', 'text-white', 'hover:bg-brand-dark');
}
if (window.showToast) window.showToast({ title: 'Success', message: data.status === 'subscribed' ? 'Subscribed successfully!' : 'Unsubscribed.' });
} else {
if (window.showToast) window.showToast({ title: 'Error', message: 'Action failed.' });
}
} catch (err) {
console.error(err);
if (window.showToast) window.showToast({ title: 'Error', message: 'Network error.' });
} finally {
btn.disabled = false;
if (spinner) spinner.classList.add('hidden');
}
});
});
</script>
{% endblock %}