aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2024-09-06 01:13:12 -0400
committerBobby <[email protected]>2024-09-06 01:13:12 -0400
commit55d87c6a2c04cd8e1e5ea215dd670f0a8c42ec19 (patch)
tree406ba2bc81900bc5ca8636a0599635bae5a78cc4
parent8aa601d5d7a8fa37ec3b905e0b94790f7e6ac191 (diff)
downloadyugen-55d87c6a2c04cd8e1e5ea215dd670f0a8c42ec19.tar.xz
yugen-55d87c6a2c04cd8e1e5ea215dd670f0a8c42ec19.zip
fancier detail page
-rw-r--r--detail/views.py12
-rw-r--r--static/css/main.css49
-rw-r--r--templates/detail/detail.html450
3 files changed, 502 insertions, 9 deletions
diff --git a/detail/views.py b/detail/views.py
index f9fbd9c..9b6908c 100644
--- a/detail/views.py
+++ b/detail/views.py
@@ -5,6 +5,7 @@ import requests
from functools import lru_cache
from authentication.utils import get_single_anime_mal
from watch.utils import get_all_episode_metadata, get_from_redis_cache, store_in_redis_cache
+from watch.views import get_seasons_by_zid
def index(request):
@@ -26,8 +27,19 @@ def detail(request, anime_id):
context = {
"anime": anime_data,
"episodes": anime_episodes,
+ "related": anime_data.get("relations", []),
+ "recommendations": anime_data.get("recommendations", []),
}
+ zid = anime_data["episodes"][0]["id"].split("$")[0] if len(anime_data["episodes"]) > 0 else None
+ if zid:
+ seasons = get_seasons_by_zid(zid)
+ if seasons:
+ context["seasons"] = seasons
+
+ if "nextAiringEpisode" in anime_data:
+ context["nextAiringEpisode"] = anime_data["nextAiringEpisode"]
+
if mal_data:
context["mal_data"] = mal_data
context["mal_episode_range"] = range(1, mal_data["num_episodes"] + 1)
diff --git a/static/css/main.css b/static/css/main.css
index 3220336..12d8fa4 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -719,6 +719,11 @@ video {
margin-bottom: 2rem;
}
+.mx-2 {
+ margin-left: 0.5rem;
+ margin-right: 0.5rem;
+}
+
.mb-2 {
margin-bottom: 0.5rem;
}
@@ -783,6 +788,10 @@ video {
display: inline-flex;
}
+.grid {
+ display: grid;
+}
+
.hidden {
display: none;
}
@@ -1048,6 +1057,10 @@ video {
list-style-position: inside;
}
+.grid-cols-1 {
+ grid-template-columns: repeat(1, minmax(0, 1fr));
+}
+
.flex-row {
flex-direction: row;
}
@@ -1114,6 +1127,12 @@ video {
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
}
+.space-x-2 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-x-reverse: 0;
+ margin-right: calc(0.5rem * var(--tw-space-x-reverse));
+ margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
+}
+
.overflow-auto {
overflow: auto;
}
@@ -1593,6 +1612,11 @@ video {
background-color: rgb(161 98 7 / var(--tw-bg-opacity));
}
+.bg-gray-800 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(31 41 55 / var(--tw-bg-opacity));
+}
+
.bg-opacity-10 {
--tw-bg-opacity: 0.1;
}
@@ -2436,6 +2460,11 @@ main {
background-color: rgb(161 98 7 / var(--tw-bg-opacity));
}
+.hover\:bg-gray-700:hover {
+ --tw-bg-opacity: 1;
+ background-color: rgb(55 65 81 / var(--tw-bg-opacity));
+}
+
.hover\:bg-opacity-30:hover {
--tw-bg-opacity: 0.3;
}
@@ -2631,6 +2660,10 @@ main {
height: 39vw;
}
+ .lg\:h-28 {
+ height: 7rem;
+ }
+
.lg\:max-h-24 {
max-height: 6rem;
}
@@ -2667,6 +2700,22 @@ main {
width: 15rem;
}
+ .lg\:w-48 {
+ width: 12rem;
+ }
+
+ .lg\:w-auto {
+ width: auto;
+ }
+
+ .lg\:flex-shrink-0 {
+ flex-shrink: 0;
+ }
+
+ .lg\:grid-cols-2 {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
.lg\:flex-row {
flex-direction: row;
}
diff --git a/templates/detail/detail.html b/templates/detail/detail.html
index 9f97aa9..5ae62ee 100644
--- a/templates/detail/detail.html
+++ b/templates/detail/detail.html
@@ -4,13 +4,20 @@
<style>
@media (max-width: 640px) {
.detail-section {
- top: 8rem;
+ top: 0rem;
left: -8.5rem;
width: 100vw;
}
+
+ .w-fix {
+ position: relative;
+ left: -8rem;
+ width: calc(100vw - 1rem);
+ }
}
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"/>
{% endblock css %}
{% block content %}
<div style="background-image: url('{{ anime.cover }}')" class="relative bg-center bg-cover h-32 lg:h-96 my-4 rounded-lg" >
@@ -170,7 +177,7 @@
{{ anime.description|strip_html }}
</p>
{% if mal_data %}
- <section class="flex flex-col lg:flex-row gap-4 my-2">
+ <section class="flex flex-col lg:flex-row gap-4 my-2 w-fix">
<div class="flex-1 flex flex-col gap-2">
<span>Update MAL Status</span>
<div class="relative w-full custom-select text-sm" data-select="status">
@@ -264,7 +271,32 @@
</div>
</section>
{% endif %}
- <section class="w-full flex flex-col items-center justify-center p-2 relative detail-section">
+ {% if seasons %}
+ <div class="my-8 w-fix">
+ <h2 class="text-xl font-bold text-white uppercase flex flex-row items-center gap-1 mb-4 ">
+ Seasons
+ </h2>
+ <div class="flex flex-row gap-2 mt-4 overflow-x-auto no-scrollbar swiper seasonSwiper">
+ <div class="swiper-wrapper">
+ {% for season in seasons %}
+ <a href="{% url "watch:watch_via_zid" season.id %}" class="group rounded-lg aspect-video h-48 relative flex-shrink-0 overflow-hidden swiper-slide
+ {% if season.isCurrent %}border-4 border-{{ user.preferences.accent_colour }}-600{% endif %}">
+ <div class="absolute inset-0 bg-center bg-cover transition-transform group-hover:scale-110" style="background-image: url('{{ season.poster }}')"></div>
+ <div class="absolute inset-0 bg-{{ user.preferences.accent_colour }}-600 opacity-0 group-hover:opacity-30 transition-opacity"></div>
+ <div class="absolute inset-0" style="background: linear-gradient(45deg, rgb(8, 8, 8) 15%, transparent 60%), linear-gradient(0deg, rgb(8, 8, 8) 0%, transparent 60%);"></div>
+ <div class="flex flex-col justify-end h-full p-2 relative z-10">
+ <h1 class="text-xl font-bold truncate max-w-full overflow-hidden text-ellipsis whitespace-nowrap">
+ {{ season.name }}
+ </h1>
+ <h2 class="font-bold truncate max-w-full overflow-hidden text-ellipsis whitespace-nowrap">{{ season.title }}</h2>
+ </div>
+ </a>
+ {% endfor %}
+ </div>
+ </div>
+ </div>
+ {% endif %}
+ <section class="w-full flex flex-col items-center justify-center p-2 relative detail-section" id="dss">
<section class="inline-flex w-max flex-row gap-4 rounded-full mb-8 bg-white bg-opacity-10 mx-auto">
<button class="flex flex-row items-center focus:outline-none gap-2 text-sm font-bold py-2 px-4 rounded-full category-switch" data-target="characters">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4">
@@ -325,23 +357,82 @@
</section>
<section id="episodes" class="w-full flex-wrap flex justify-start">
{% for episode in episodes.episodes %}
- <a href="{% url "watch:watch_episode" anime.id episode.number %}" class="w-full lg:w-1/2 px-2 mb-2">
+ <a href="{% url "watch:watch_episode" anime.id episode.number %}" class="w-full lg:w-1/2 px-2 mb-2 episode-item">
<div class="flex flex-col lg:flex-row w-full bg-neutral-950 rounded hover:bg-{{ user.preferences.accent_colour }}-600 hover:bg-opacity-30 p-2 gap-4">
- <div class="flex-shrink-0">
- <img src="{% if episode.metadata.image %}{{ episode.metadata.image }}{% else %}{{ anime.cover}}{% endif %}" alt="{{ episode.title }}" class="w-48 h-28 object-cover rounded-lg"/>
+ <div class="w-full lg:w-auto lg:flex-shrink-0">
+ <img src="{% if episode.metadata.image %}{{ episode.metadata.image }}{% else %}{{ anime.cover}}{% endif %}" alt="{{ episode.title }}" class="w-full lg:w-48 lg:h-28 object-cover rounded-lg"/>
</div>
<div class="flex flex-col gap-2">
- <h2 class="font-bold">{% if episode.metadata.title %}{{ episode.metadata.title }}{% else %}{{ episode.title }}{% endif %}</h2>
+ <h2 class="font-bold">{{episode.number}}. {% if episode.metadata.title %}{{ episode.metadata.title }}{% else %}{{ episode.title }}{% endif %}</h2>
<p class="text-sm">{{ episode.metadata.description|truncatewords:50 }}</p>
</div>
</div>
</a>
{% endfor %}
{% if not episodes %}
- <div class="w-full h-96 flex items-center justify-center">
- <span>No Episodes Available</span>
+ <div class="w-full h-96 flex flex-col gap-2 items-center justify-center">
+ <p>No Episodes Available</p>
</div>
{% endif %}
+ {% if nextAiringEpisode %}
+ <p class="w-full text-center my-2" id="nextAiringEpisodeMessage"></p>
+ <script>
+ const nextAiringEpisode = {
+ airingTime: {{ nextAiringEpisode.airingTime }},
+ timeUntilAiring: {{ nextAiringEpisode.timeUntilAiring }},
+ episode: {{ nextAiringEpisode.episode }}
+ };
+
+ function formatNextEpisode(nextAiringEpisode) {
+ const airingTime = nextAiringEpisode.airingTime * 1000; // Convert to milliseconds
+ const episodeNumber = nextAiringEpisode.episode;
+
+ function updateCountdown() {
+ const now = new Date().getTime();
+ const timeUntilAiring = airingTime - now;
+
+ if (timeUntilAiring <= 0) {
+ return `Episode ${episodeNumber} has aired.`;
+ }
+
+ const days = Math.floor(timeUntilAiring / (1000 * 60 * 60 * 24));
+ const hours = Math.floor((timeUntilAiring % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
+ const minutes = Math.floor((timeUntilAiring % (1000 * 60 * 60)) / (1000 * 60));
+ const seconds = Math.floor((timeUntilAiring % (1000 * 60)) / 1000);
+
+ const airingDate = new Date(airingTime);
+ const formattedDate = airingDate.toLocaleDateString('en-US', {
+ weekday: 'short',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ });
+ const formattedTime = airingDate.toLocaleTimeString('en-US', {
+ hour: 'numeric',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: true
+ });
+
+ return `Episode <strong>${episodeNumber}</strong> will air on ${formattedDate} at ${formattedTime} ` +
+ `<strong class="text-{{ user.preferences.accent_colour }}-600">(${days} days, ${hours} hours, ${minutes} minutes, ${seconds} seconds)</strong>`;
+ }
+
+ const countdownElement = document.getElementById('nextAiringEpisodeMessage');
+ countdownElement.innerHTML = updateCountdown();
+
+ // Update countdown every second
+ setInterval(() => {
+ countdownElement.innerHTML = updateCountdown();
+ }, 1000);
+ }
+
+ // Call the function when the page loads
+ document.addEventListener('DOMContentLoaded', function() {
+ formatNextEpisode(nextAiringEpisode);
+ });
+ </script>
+ {% endif %}
</section>
<section id="trailer" class="w-full hidden">
{% if anime.trailer and anime.trailer.site == "youtube" %}
@@ -353,6 +444,224 @@
{% endif %}
</section>
</section>
+ <div class="w-full w-fix">
+ {% if related %}
+ <div class="text-xl font-bold text-white uppercase flex flex-row items-center gap-1 mb-4">
+ <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 256 512" class="size-4" xmlns="http://www.w3.org/2000/svg"><path d="M224.3 273l-136 136c-9.4 9.4-24.6 9.4-33.9 0l-22.6-22.6c-9.4-9.4-9.4-24.6 0-33.9l96.4-96.4-96.4-96.4c-9.4-9.4-9.4-24.6 0-33.9L54.3 103c9.4-9.4 24.6-9.4 33.9 0l136 136c9.5 9.4 9.5 24.6.1 34z"></path></svg>
+ <span>Related</span>
+ </div>
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-2">
+ {% for related in related|slice:":5" %}
+ {% if related.type == "MANGA" or related.type == "NOVEL" %}
+ <div onClick="showToast('{% if related.type == "MANGA" %}Manga{% else %}Novel{% endif %} reading is not supported yet!', false)" class="cursor-pointer flex flex-row w-full gap-4 bg-white bg-opacity-10 p-2 rounded hover:bg-{{ user.preferences.accent_colour }}-600 hover:bg-opacity-30">
+ <img src="{{ related.image }}" alt="{{ related.title.english }}" class="rounded-lg w-12 h-16 object-cover"/>
+ <div class="flex flex-col gap-2">
+ <span class="font-bold flex gap-2 flex-row items-center">
+ {% if related.status == "Ongoing" %}
+ <span class="text-green-500">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-2 sm:size-3">
+ <circle cx="12" cy="12" r="12" />
+ </svg>
+ </span>
+ {% elif related.status == "Not yet aired" %}
+ <span class="text-yellow-500">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-2 sm:size-3">
+ <circle cx="12" cy="12" r="12" />
+ </svg>
+ </span>
+ {% else %}
+ <span class="text-blue-500">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-2 sm:size-3">
+ <circle cx="12" cy="12" r="12" />
+ </svg>
+ </span>
+ {% endif %}
+ <span>
+ {% if user.preferences.title_language == "english" and related.title.english %}
+ {{ related.title.english }}
+ {% elif user.preferences.title_language == "native" and related.title.native %}
+ {{ related.title.native }}
+ {% else %}
+ {{ related.title.romaji }}
+ {% endif %}
+ </span>
+ </span>
+ <span class="text-xs sm:text-sm font-bold flex gap-1 flex-row items-start">
+ <span class="text-xs font-bold bg-white bg-opacity-10 p-1 rounded flex items-center gap-1">
+ <svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" class="mr-1">
+ <path d="M3.604 7.197l7.138 -3.109a.96 .96 0 0 1 1.27 .527l4.924 11.902a1 1 0 0 1 -.514 1.304l-7.137 3.109a.96 .96 0 0 1 -1.271 -.527l-4.924 -11.903a1 1 0 0 1 .514 -1.304z"></path>
+ <path d="M15 4h1a1 1 0 0 1 1 1v3.5"></path>
+ <path d="M20 6c.264 .112 .52 .217 .768 .315a1 1 0 0 1 .53 1.311l-2.298 5.374"></path>
+ </svg>
+ {{ related.type }}
+ </span>
+ <span class="text-xs font-bold bg-white bg-opacity-10 p-1 rounded flex items-center gap-1">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4">
+ <path d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z" />
+ </svg>
+ {{ related.relationType }}
+ </span>
+ {% if related.rating %}
+ <span class="text-xs font-bold bg-white bg-opacity-10 p-1 rounded flex items-center gap-1">
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z"/>
+ </svg>
+ {{ related.rating }}
+ </span>
+ {% endif %}
+ </span>
+ </div>
+ </div>
+ {% endif %}
+ {% if related.type == "TV" or related.type == "MOVIE" or related.type == "OVA" or related.type == "ONA" or related.type == "SPECIAL" or related.type == "TV_SHORT" %}
+ <a href="{% if not viaMal %}{% url 'watch:watch' related.id %}{% else %}{% url "watch:watch_via_zid" related.zid %}{% endif %}" class="flex flex-row w-full gap-4 bg-white bg-opacity-10 p-2 rounded hover:bg-{{ user.preferences.accent_colour }}-600 hover:bg-opacity-30">
+ <img src="{{ related.image }}" alt="{{ related.title.english }}" class="rounded-lg w-12 h-16 object-cover"/>
+ <div class="flex flex-col gap-2">
+ <span class="font-bold flex gap-2 flex-row items-center">
+ {% if related.status == "Ongoing" %}
+ <span class="text-green-500">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-2 sm:size-3">
+ <circle cx="12" cy="12" r="12" />
+ </svg>
+ </span>
+ {% elif related.status == "Not yet aired" %}
+ <span class="text-yellow-500">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-2 sm:size-3">
+ <circle cx="12" cy="12" r="12" />
+ </svg>
+ </span>
+ {% else %}
+ <span class="text-blue-500">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-2 sm:size-3">
+ <circle cx="12" cy="12" r="12" />
+ </svg>
+ </span>
+ {% endif %}
+ <span>
+ {% if user.preferences.title_language == "english" and related.title.english %}
+ {{ related.title.english }}
+ {% elif user.preferences.title_language == "native" and related.title.native %}
+ {{ related.title.native }}
+ {% else %}
+ {{ related.title.romaji }}
+ {% endif %}
+ </span>
+ </span>
+ <span class="text-xs sm:text-sm font-bold flex gap-1 flex-row items-start">
+ <span class="text-xs font-bold bg-white bg-opacity-10 p-1 rounded flex items-center gap-1">
+ <svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" class="mr-1">
+ <path d="M3.604 7.197l7.138 -3.109a.96 .96 0 0 1 1.27 .527l4.924 11.902a1 1 0 0 1 -.514 1.304l-7.137 3.109a.96 .96 0 0 1 -1.271 -.527l-4.924 -11.903a1 1 0 0 1 .514 -1.304z"></path>
+ <path d="M15 4h1a1 1 0 0 1 1 1v3.5"></path>
+ <path d="M20 6c.264 .112 .52 .217 .768 .315a1 1 0 0 1 .53 1.311l-2.298 5.374"></path>
+ </svg>
+ {{ related.type }}
+ </span>
+ {% if related.rating %}
+ <span class="text-xs font-bold bg-white bg-opacity-10 p-1 rounded flex items-center gap-1">
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z"/>
+ </svg>
+ {{ related.rating }}
+ </span>
+ {% endif %}
+ {% if related.relationType %}
+ <span class="text-xs font-bold bg-white bg-opacity-10 p-1 rounded flex items-center gap-1">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4">
+ <path d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z" />
+ </svg>
+ {{ related.relationType }}
+ </span>
+ {% endif %}
+ {% if related.episodes %}
+ <span class="text-xs font-bold bg-white bg-opacity-10 p-1 rounded flex items-center gap-1">
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5-3.9 19.5m-2.1-19.5-3.9 19.5"/>
+ </svg>
+ {{ related.episodes }}
+ </span>
+ {% endif %}
+ </span>
+ </div>
+ </a>
+ {% endif %}
+ {% endfor %}
+ </div>
+ {% endif %}
+ {% if recommendations %}
+ <div class="text-xl font-bold text-white uppercase flex flex-row items-center gap-1 my-4">
+ <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 256 512" class="size-4" xmlns="http://www.w3.org/2000/svg"><path d="M224.3 273l-136 136c-9.4 9.4-24.6 9.4-33.9 0l-22.6-22.6c-9.4-9.4-9.4-24.6 0-33.9l96.4-96.4-96.4-96.4c-9.4-9.4-9.4-24.6 0-33.9L54.3 103c9.4-9.4 24.6-9.4 33.9 0l136 136c9.5 9.4 9.5 24.6.1 34z"></path></svg>
+ <span>Recommendations</span>
+ </div>
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-2">
+ {% for recommendation in recommendations|slice:":10" %}
+ {% if recommendation.id or recommendation.zid %}
+ <a href="{% if not viaMal %}{% url 'watch:watch' recommendation.id %}{% else %}{% url "watch:watch_via_zid" recommendation.zid %}{% endif %}" class="flex flex-row w-full gap-4 bg-white bg-opacity-10 p-2 rounded hover:bg-{{ user.preferences.accent_colour }}-600 hover:bg-opacity-30">
+ <img src="{{ recommendation.image }}" alt="{{ recommendation.title.english }}" class="rounded-lg w-12 h-16 object-cover"/>
+ <div class="flex flex-col gap-2 max-w-[calc(100%-4rem)]">
+ <span class="font-bold flex gap-2 flex-row items-center">
+ {% if recommendation.status == "Ongoing" %}
+ <span class="text-green-500">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-2 sm:size-3">
+ <circle cx="12" cy="12" r="12" />
+ </svg>
+ </span>
+ {% elif recommendation.status == "Not yet aired" %}
+ <span class="text-yellow-500">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-2 sm:size-3">
+ <circle cx="12" cy="12" r="12" />
+ </svg>
+ </span>
+ {% else %}
+ <span class="text-blue-500">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-2 sm:size-3">
+ <circle cx="12" cy="12" r="12" />
+ </svg>
+ </span>
+ {% endif %}
+ <span class="truncate max-w-full overflow-hidden text-ellipsis whitespace-nowrap">
+ {% if user.preferences.title_language == "english" and recommendation.title.english %}
+ {{ recommendation.title.english }}
+ {% elif user.preferences.title_language == "native" and recommendation.title.native %}
+ {{ recommendation.title.native }}
+ {% else %}
+ {{ recommendation.title.romaji }}
+ {% endif %}
+ </span>
+ </span>
+ <span class="text-xs sm:text-sm font-bold flex gap-1 flex-row items-start">
+ <span class="text-xs font-bold bg-white bg-opacity-10 p-1 rounded flex items-center gap-1">
+ <svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" class="mr-1">
+ <path d="M3.604 7.197l7.138 -3.109a.96 .96 0 0 1 1.27 .527l4.924 11.902a1 1 0 0 1 -.514 1.304l-7.137 3.109a.96 .96 0 0 1 -1.271 -.527l-4.924 -11.903a1 1 0 0 1 .514 -1.304z"></path>
+ <path d="M15 4h1a1 1 0 0 1 1 1v3.5"></path>
+ <path d="M20 6c.264 .112 .52 .217 .768 .315a1 1 0 0 1 .53 1.311l-2.298 5.374"></path>
+ </svg>
+ {{ recommendation.type }}
+ </span>
+ {% if recommendation.rating %}
+ <span class="text-xs font-bold bg-white bg-opacity-10 p-1 rounded flex items-center gap-1">
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a
+ .563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z"/>
+ </svg>
+ {{ recommendation.rating }}
+ </span>
+ {% if recommendation.episodes %}
+ <span class="text-xs font-bold bg-white bg-opacity-10 p-1 rounded flex items-center gap-1">
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5-3.9 19.5m-2.1-19.5-3.9 19.5"/>
+ </svg>
+ {{ recommendation.episodes }}
+ </span>
+ {% endif %}
+ </span>
+ {% endif %}
+ </div>
+ </a>
+ {% endif %}
+ {% endfor %}
+ </div>
+ {% endif %}
+ </div>
</div>
</div>
<div id="toastContainer" class="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50 flex flex-col space-y-2"></div>
@@ -485,4 +794,127 @@
observer.observe(episodesSelect, {childList: true, characterData: true});
{% endif %}
</script>
+<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
+<script>
+ const seasonSwiper = new Swiper(".seasonSwiper", {
+ slidesPerView: 'auto',
+ spaceBetween: 8,
+ freeMode: true,
+ });
+</script>
+<script>
+ // Configuration
+ const EPISODES_PER_PAGE = 10;
+
+ // Get all episodes
+ const allEpisodes = Array.from(document.querySelectorAll('#episodes > a'));
+ let currentPage = 1;
+ let totalPages = Math.ceil(allEpisodes.length / EPISODES_PER_PAGE);
+
+ function showEpisodesForPage(page) {
+ const startIndex = (page - 1) * EPISODES_PER_PAGE;
+ const endIndex = startIndex + EPISODES_PER_PAGE;
+
+ allEpisodes.forEach((episode, index) => {
+ episode.style.display = (index >= startIndex && index < endIndex) ? '' : 'none';
+ });
+ }
+
+ function createPaginationControls() {
+ // Create a container for pagination that's separate from the episodes
+ const paginationContainer = document.createElement('div');
+ paginationContainer.className = 'w-full'; // Ensure it takes full width
+
+ const section = document.createElement('section');
+ section.className = 'flex justify-center items-center my-8';
+
+ const nav = document.createElement('nav');
+ nav.className = 'inline-flex rounded-md shadow-sm transform scale-[0.80] sm:scale-100';
+ nav.style.transformOrigin = 'center';
+ nav.setAttribute('aria-label', 'Pagination');
+
+ const buttons = [
+ { text: 'First', condition: () => currentPage > 1, page: 1 },
+ { text: 'Previous', condition: () => currentPage > 1, page: currentPage - 1 },
+ { text: `${currentPage} of ${totalPages}`, condition: null },
+ { text: 'Next', condition: () => currentPage < totalPages, page: currentPage + 1 },
+ { text: 'Last', condition: () => currentPage < totalPages, page: totalPages }
+ ];
+
+ buttons.forEach((button, index) => {
+ const element = button.condition !== null
+ ? createButton(button.text, button.condition(), button.page, index)
+ : createSpan(button.text, index);
+ nav.appendChild(element);
+ });
+
+ section.appendChild(nav);
+ paginationContainer.appendChild(section);
+
+ // Insert the pagination container after the episodes container
+ const episodesSection = document.getElementById('episodes');
+ episodesSection.parentNode.insertBefore(paginationContainer, episodesSection.nextSibling);
+ }
+
+ function createButton(text, isEnabled, page, index) {
+ const element = isEnabled ? document.createElement('a') : document.createElement('span');
+ let className = `w-24 px-3 py-2 text-sm font-medium flex items-center justify-center `;
+
+ if (index === 0) className += 'rounded-l-md ';
+ if (index === 4) className += 'rounded-r-md ';
+
+ if (isEnabled) {
+ className += `bg-{{ user.preferences.accent_colour }}-600 bg-opacity-30 text-white hover:bg-opacity-40 `;
+ if (index === 1 || index === 3) {
+ className += 'text-opacity-90 hover:text-opacity-100 ';
+ }
+ element.href = '#';
+ element.addEventListener('click', (e) => {
+ e.preventDefault();
+ goToPage(page);
+ });
+ } else {
+ className += 'bg-{{ user.preferences.accent_colour }}-600 bg-opacity-10 text-white text-opacity-30 cursor-not-allowed';
+ }
+
+ element.className = className;
+ element.textContent = text;
+ return element;
+ }
+
+ function createSpan(text) {
+ const span = document.createElement('span');
+ span.className = 'w-28 px-3 py-2 bg-{{ user.preferences.accent_colour }}-600 bg-opacity-50 text-sm font-medium text-white flex items-center justify-center';
+ span.textContent = text;
+ return span;
+ }
+
+ function smoothScrollToEpisodes() {
+ const episodesSection = document.getElementById('dss');
+ episodesSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+
+ function goToPage(page) {
+ if (page >= 1 && page <= totalPages && page !== currentPage) {
+ currentPage = page;
+ updatePagination();
+ smoothScrollToEpisodes();
+ }
+ }
+
+ function updatePagination() {
+ showEpisodesForPage(currentPage);
+ const paginationContainer = document.querySelector('#episodes + div');
+ if (paginationContainer) {
+ paginationContainer.remove();
+ }
+ createPaginationControls();
+ }
+
+ // Initialize pagination
+ document.addEventListener('DOMContentLoaded', function() {
+ createPaginationControls();
+ showEpisodesForPage(currentPage);
+ });
+</script>
{% endblock scripts %} \ No newline at end of file