mirror of
https://github.com/barkeser2002/offline-db.git
synced 2026-09-25 00:59:54 +03:00
- 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>
176 lines
8.8 KiB
HTML
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 %}
|