aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--homepage/urls.py1
-rw-r--r--homepage/utils.py110
-rw-r--r--homepage/views.py57
-rw-r--r--requirements.txt1
-rw-r--r--static/css/main.css366
-rw-r--r--templates/home/schedule.html192
-rw-r--r--templates/partials/navbar.html8
7 files changed, 699 insertions, 36 deletions
diff --git a/homepage/urls.py b/homepage/urls.py
index 9fab287..d187864 100644
--- a/homepage/urls.py
+++ b/homepage/urls.py
@@ -6,5 +6,6 @@ app_name = "home"
urlpatterns = [
path("", views.index, name="index"),
path("search", views.search, name="search"),
+ path("schedule", views.schedule, name="schedule"),
path("q_search", views.search_json, name="q_search"),
]
diff --git a/homepage/utils.py b/homepage/utils.py
index 2c238bb..336a80d 100644
--- a/homepage/utils.py
+++ b/homepage/utils.py
@@ -1,7 +1,12 @@
+from collections import defaultdict
import os
import dotenv
import requests
import time
+import datetime
+import pytz
+import requests
+from django.utils import timezone
dotenv.load_dotenv()
@@ -80,3 +85,108 @@ def get_upcoming_anime(page=1, per_page=6):
request_url = f"{CONSUMET_URL}/meta/anilist/advanced-search?type=ANIME&status=NOT_YET_RELEASED&sort=[%22POPULARITY_DESC%22]&season={season}&year={year}&?page={page}&perPage={per_page}"
response = requests.get(request_url)
return response.json()
+
+def get_start_end_times():
+ now = datetime.datetime.now(pytz.UTC)
+
+ start_time = now.replace(hour=4, minute=0, second=0, microsecond=0)
+ if now.hour < 4:
+ start_time -= datetime.timedelta(days=1)
+
+ times = []
+ for i in range(7):
+ start = start_time + datetime.timedelta(days=i)
+ end = start + datetime.timedelta(days=1)
+ times.append({
+ "start": int(start.timestamp()),
+ "end": int(end.timestamp()),
+ "day": start.strftime("%A"),
+ "date": start.strftime("%Y-%m-%d"),
+ "today": i == 0
+ })
+
+ return times
+
+def get_anime_schedule(week_start, week_end):
+ # GraphQL query
+ query = """
+ query ($weekStart: Int, $weekEnd: Int, $page: Int, $perPage: Int) {
+ Page(page: $page, perPage: $perPage) {
+ pageInfo {
+ hasNextPage
+ total
+ currentPage
+ lastPage
+ perPage
+ }
+ airingSchedules(
+ airingAt_greater: $weekStart
+ airingAt_lesser: $weekEnd
+ sort: TIME
+ ) {
+ id
+ episode
+ timeUntilAiring
+ airingAt
+ mediaId
+ media {
+ isAdult
+ title {
+ romaji
+ native
+ english
+ }
+ coverImage {
+ extraLarge
+ medium
+ color
+ }
+ bannerImage
+ siteUrl
+ }
+ }
+ }
+ }
+ """
+
+ # Variables for the query
+ variables = {
+ "weekStart": week_start,
+ "weekEnd": week_end,
+ "page": 1,
+ "perPage": 50 # Adjust as needed
+ }
+
+ # Make the request to the AniList API
+ url = 'https://graphql.anilist.co'
+ response = requests.post(url, json={'query': query, 'variables': variables})
+
+ if response.status_code == 200:
+ return response.json()['data']['Page']['airingSchedules']
+ else:
+ return None
+
+def group_anime_schedules(schedules):
+ grouped = defaultdict(list)
+ for schedule in schedules:
+ airing_time = datetime.datetime.fromtimestamp(schedule['airingAt'])
+ grouped[airing_time].append(schedule)
+ return dict(sorted(grouped.items()))
+
+def find_target_anime(grouped_schedules):
+ flat_list = [
+ (time, anime)
+ for time, animes in grouped_schedules.items()
+ for anime in animes
+ ]
+ flat_list.sort(key=lambda x: x[0]) # Sort by airing time
+
+ first_positive = next((i for i, (_, anime) in enumerate(flat_list) if anime['timeUntilAiring'] > 0), None)
+
+ if first_positive is None or first_positive < 2:
+ return None, []
+
+ target = flat_list[first_positive][1]
+ last_two = [anime for _, anime in flat_list[first_positive-2:first_positive]]
+
+ return target, last_two \ No newline at end of file
diff --git a/homepage/views.py b/homepage/views.py
index ddceb56..ee94790 100644
--- a/homepage/views.py
+++ b/homepage/views.py
@@ -6,12 +6,16 @@ from datetime import datetime
from watch.utils import get_from_redis_cache, store_in_redis_cache
import json
from homepage.utils import (
+ find_target_anime,
+ get_anime_schedule,
+ get_start_end_times,
get_trending_anime,
get_popular_anime,
get_top_anime,
get_top_airing_anime,
get_upcoming_anime,
get_next_season,
+ group_anime_schedules,
)
from functools import lru_cache
from user_profile.models import UserHistory
@@ -120,3 +124,56 @@ def search_json(request):
search_results = response.json()
return JsonResponse(search_results)
+
+def schedule(request):
+ start = request.GET.get("start")
+ end = request.GET.get("end")
+
+ times = get_start_end_times()
+ today = times[
+ next(
+ (
+ index
+ for index, time in enumerate(times)
+ if time["today"]
+ ),
+ None,
+ )
+ ]
+
+ schedule_data = get_anime_schedule(today["start"], today["end"])
+ grouped_schedule_data = group_anime_schedules(schedule_data)
+ target, last_two = find_target_anime(grouped_schedule_data)
+
+ if start and end:
+ today = next(
+ (
+ time
+ for time in times
+ if time["start"] == int(start) and time["end"] == int(end)
+ ),
+ None,
+ )
+
+ if today:
+ schedule_data = get_anime_schedule(today["start"], today["end"])
+ grouped_schedule_data = group_anime_schedules(schedule_data)
+
+ # change today in times
+ for time in times:
+ time["today"] = False
+ today["today"] = True
+ else:
+ start = today["start"]
+ end = today["end"]
+
+ context = {
+ "schedule": grouped_schedule_data,
+ "next_airing": target,
+ "recently_aired": last_two,
+ "times": times,
+ "start": start,
+ "end": end,
+ }
+
+ return render(request, "home/schedule.html", context)
diff --git a/requirements.txt b/requirements.txt
index 0e69157..fd7b072 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,3 +5,4 @@ requests
django-cors-headers
redis
gunicorn
+pytz
diff --git a/static/css/main.css b/static/css/main.css
index 12d8fa4..4223bdf 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -677,6 +677,34 @@ video {
top: 100%;
}
+.left-\[-0\.5rem\] {
+ left: -0.5rem;
+}
+
+.top-\[-0\.35rem\] {
+ top: -0.35rem;
+}
+
+.-left-\[0\.5rem\] {
+ left: -0.5rem;
+}
+
+.-top-\[0\.35rem\] {
+ top: -0.35rem;
+}
+
+.-left-\[0\.35rem\] {
+ left: -0.35rem;
+}
+
+.top-1 {
+ top: 0.25rem;
+}
+
+.z-0 {
+ z-index: 0;
+}
+
.z-10 {
z-index: 10;
}
@@ -689,6 +717,14 @@ video {
z-index: 50;
}
+.z-30 {
+ z-index: 30;
+}
+
+.z-\[3\] {
+ z-index: 3;
+}
+
.-mx-4 {
margin-left: -1rem;
margin-right: -1rem;
@@ -719,9 +755,8 @@ video {
margin-bottom: 2rem;
}
-.mx-2 {
- margin-left: 0.5rem;
- margin-right: 0.5rem;
+.-ml-5 {
+ margin-left: -1.25rem;
}
.mb-2 {
@@ -833,10 +868,6 @@ video {
height: 6rem;
}
-.h-28 {
- height: 7rem;
-}
-
.h-32 {
height: 8rem;
}
@@ -877,6 +908,10 @@ video {
height: 100%;
}
+.h-\[2rem\] {
+ height: 2rem;
+}
+
.max-h-24 {
max-height: 6rem;
}
@@ -921,10 +956,6 @@ video {
width: 33.333333%;
}
-.w-48 {
- width: 12rem;
-}
-
.w-5 {
width: 1.25rem;
}
@@ -958,10 +989,30 @@ video {
width: max-content;
}
+.w-20 {
+ width: 5rem;
+}
+
+.w-\[2\.5rem\] {
+ width: 2.5rem;
+}
+
+.w-1\/4 {
+ width: 25%;
+}
+
+.w-60 {
+ width: 15rem;
+}
+
.min-w-32 {
min-width: 8rem;
}
+.min-w-0 {
+ min-width: 0px;
+}
+
.max-w-7xl {
max-width: 80rem;
}
@@ -987,18 +1038,50 @@ video {
max-width: max-content;
}
+.max-w-\[calc\(100\%-4\.5rem\)\] {
+ max-width: calc(100% - 4.5rem);
+}
+
+.max-w-\[calc\(100\%-8rem\)\] {
+ max-width: calc(100% - 8rem);
+}
+
+.max-w-96 {
+ max-width: 24rem;
+}
+
+.max-w-\[calc\(100\%-2rem\)\] {
+ max-width: calc(100% - 2rem);
+}
+
.flex-1 {
flex: 1 1 0%;
}
+.flex-none {
+ flex: none;
+}
+
.flex-shrink-0 {
flex-shrink: 0;
}
+.flex-grow {
+ flex-grow: 1;
+}
+
.origin-left {
transform-origin: left;
}
+.origin-top-left {
+ transform-origin: top left;
+}
+
+.origin-\[0px_0px\] {
+ transform-origin: 0px 0px;
+}
+
.-translate-x-1\/2 {
--tw-translate-x: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@@ -1009,6 +1092,11 @@ video {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
+.rotate-\[-45deg\] {
+ --tw-rotate: -45deg;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
.scale-\[0\.80\] {
--tw-scale-x: 0.80;
--tw-scale-y: 0.80;
@@ -1039,6 +1127,16 @@ video {
animation: spin 1s linear infinite;
}
+@keyframes pulse {
+ 50% {
+ opacity: .5;
+ }
+}
+
+.animate-pulse {
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+}
+
.cursor-not-allowed {
cursor: not-allowed;
}
@@ -1127,12 +1225,6 @@ 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;
}
@@ -1149,6 +1241,10 @@ video {
overflow-y: auto;
}
+.overflow-x-scroll {
+ overflow-x: scroll;
+}
+
.overflow-y-scroll {
overflow-y: scroll;
}
@@ -1201,6 +1297,23 @@ video {
border-bottom-right-radius: 0.375rem;
}
+.rounded-b-lg {
+ border-bottom-right-radius: 0.5rem;
+ border-bottom-left-radius: 0.5rem;
+}
+
+.rounded-br-lg {
+ border-bottom-right-radius: 0.5rem;
+}
+
+.rounded-tr-\[0\.5rem\] {
+ border-top-right-radius: 0.5rem;
+}
+
+.rounded-br-\[0\.5rem\] {
+ border-bottom-right-radius: 0.5rem;
+}
+
.border {
border-width: 1px;
}
@@ -1209,6 +1322,10 @@ video {
border-width: 4px;
}
+.border-2 {
+ border-width: 2px;
+}
+
.border-b {
border-bottom-width: 1px;
}
@@ -1348,6 +1465,11 @@ video {
border-color: rgb(202 138 4 / var(--tw-border-opacity));
}
+.border-\[\#ff8da1\] {
+ --tw-border-opacity: 1;
+ border-color: rgb(255 141 161 / var(--tw-border-opacity));
+}
+
.border-opacity-10 {
--tw-border-opacity: 0.1;
}
@@ -1612,9 +1734,39 @@ video {
background-color: rgb(161 98 7 / var(--tw-bg-opacity));
}
-.bg-gray-800 {
+.bg-pink-200 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(251 207 232 / var(--tw-bg-opacity));
+}
+
+.bg-pink-300 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(249 168 212 / var(--tw-bg-opacity));
+}
+
+.bg-pink-500 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(236 72 153 / var(--tw-bg-opacity));
+}
+
+.bg-red-300 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(252 165 165 / var(--tw-bg-opacity));
+}
+
+.bg-yellow-300 {
--tw-bg-opacity: 1;
- background-color: rgb(31 41 55 / var(--tw-bg-opacity));
+ background-color: rgb(253 224 71 / var(--tw-bg-opacity));
+}
+
+.bg-orange-300 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(253 186 116 / var(--tw-bg-opacity));
+}
+
+.bg-gray-500 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(107 114 128 / var(--tw-bg-opacity));
}
.bg-opacity-10 {
@@ -1676,6 +1828,10 @@ video {
padding: 1rem;
}
+.p-\[0\.2rem\] {
+ padding: 0.2rem;
+}
+
.px-1 {
padding-left: 0.25rem;
padding-right: 0.25rem;
@@ -1726,10 +1882,23 @@ video {
padding-bottom: 2rem;
}
+.px-8 {
+ padding-left: 2rem;
+ padding-right: 2rem;
+}
+
+.pb-2 {
+ padding-bottom: 0.5rem;
+}
+
.pb-4 {
padding-bottom: 1rem;
}
+.pl-2 {
+ padding-left: 0.5rem;
+}
+
.pl-4 {
padding-left: 1rem;
}
@@ -1750,10 +1919,38 @@ video {
padding-top: 0.25rem;
}
+.pt-2 {
+ padding-top: 0.5rem;
+}
+
.pt-\[140\%\] {
padding-top: 140%;
}
+.pr-16 {
+ padding-right: 4rem;
+}
+
+.pt-4 {
+ padding-top: 1rem;
+}
+
+.pb-3 {
+ padding-bottom: 0.75rem;
+}
+
+.pl-3 {
+ padding-left: 0.75rem;
+}
+
+.pr-9 {
+ padding-right: 2.25rem;
+}
+
+.pt-3 {
+ padding-top: 0.75rem;
+}
+
.text-center {
text-align: center;
}
@@ -2095,6 +2292,16 @@ video {
color: rgb(161 98 7 / var(--tw-text-opacity));
}
+.text-pink-500 {
+ --tw-text-opacity: 1;
+ color: rgb(236 72 153 / var(--tw-text-opacity));
+}
+
+.text-gray-300 {
+ --tw-text-opacity: 1;
+ color: rgb(209 213 219 / var(--tw-text-opacity));
+}
+
.text-opacity-30 {
--tw-text-opacity: 0.3;
}
@@ -2129,6 +2336,24 @@ video {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
+.shadow-\[2px_2px_4px_var\(--global-shadow\)\] {
+ --tw-shadow: 2px 2px 4px var(--global-shadow);
+ --tw-shadow-colored: 2px 2px 4px var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
+.shadow-\[2px_2px_4px_rgba\(0\2c 0\2c 0\2c 0\.2\)\] {
+ --tw-shadow: 2px 2px 4px rgba(0,0,0,0.2);
+ --tw-shadow-colored: 2px 2px 4px var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
+.shadow-\[-2px_2px_4px_rgba\(0\2c 0\2c 0\2c 0\.2\)\] {
+ --tw-shadow: -2px 2px 4px rgba(0,0,0,0.2);
+ --tw-shadow-colored: -2px 2px 4px var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
.outline-none {
outline: 2px solid transparent;
outline-offset: 2px;
@@ -2156,6 +2381,14 @@ video {
transition-duration: 150ms;
}
+.transition {
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
.duration-100 {
transition-duration: 100ms;
}
@@ -2260,6 +2493,28 @@ main {
animation: fadeInUp 0.5s ease-in-out forwards;
}
+.hover\:scale-50:hover {
+ --tw-scale-x: .5;
+ --tw-scale-y: .5;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.hover\:scale-105:hover {
+ --tw-scale-x: 1.05;
+ --tw-scale-y: 1.05;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.hover\:scale-x-105:hover {
+ --tw-scale-x: 1.05;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.hover\:scale-y-105:hover {
+ --tw-scale-y: 1.05;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
.hover\:rounded-b-lg:hover {
border-bottom-right-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
@@ -2460,11 +2715,6 @@ 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;
}
@@ -2491,6 +2741,12 @@ main {
--tw-text-opacity: 1;
}
+.hover\:shadow-lg:hover {
+ --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
+ --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
.focus\:outline-none:focus {
outline: 2px solid transparent;
outline-offset: 2px;
@@ -2581,6 +2837,26 @@ main {
height: 1rem;
}
+ .sm\:w-full {
+ width: 100%;
+ }
+
+ .sm\:w-96 {
+ width: 24rem;
+ }
+
+ .sm\:max-w-96 {
+ max-width: 24rem;
+ }
+
+ .sm\:max-w-none {
+ max-width: none;
+ }
+
+ .sm\:max-w-\[24rem\] {
+ max-width: 24rem;
+ }
+
.sm\:scale-100 {
--tw-scale-x: 1;
--tw-scale-y: 1;
@@ -2591,6 +2867,14 @@ main {
gap: 0.5rem;
}
+ .sm\:overflow-x-auto {
+ overflow-x: auto;
+ }
+
+ .sm\:overflow-x-hidden {
+ overflow-x: hidden;
+ }
+
.sm\:p-2 {
padding: 0.5rem;
}
@@ -2610,6 +2894,11 @@ main {
padding-bottom: 0.5rem;
}
+ .sm\:px-0 {
+ padding-left: 0px;
+ padding-right: 0px;
+ }
+
.sm\:text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
@@ -2629,6 +2918,10 @@ main {
.md\:w-64 {
width: 16rem;
}
+
+ .md\:w-auto {
+ width: auto;
+ }
}
@media (min-width: 1024px) {
@@ -2652,6 +2945,10 @@ main {
height: 4rem;
}
+ .lg\:h-28 {
+ height: 7rem;
+ }
+
.lg\:h-96 {
height: 24rem;
}
@@ -2660,10 +2957,6 @@ main {
height: 39vw;
}
- .lg\:h-28 {
- height: 7rem;
- }
-
.lg\:max-h-24 {
max-height: 6rem;
}
@@ -2696,18 +2989,27 @@ main {
width: 75%;
}
- .lg\:w-60 {
- width: 15rem;
- }
-
.lg\:w-48 {
width: 12rem;
}
+ .lg\:w-60 {
+ width: 15rem;
+ }
+
.lg\:w-auto {
width: auto;
}
+ .lg\:max-w-full {
+ max-width: 100%;
+ }
+
+ .lg\:max-w-fit {
+ max-width: -moz-fit-content;
+ max-width: fit-content;
+ }
+
.lg\:flex-shrink-0 {
flex-shrink: 0;
}
diff --git a/templates/home/schedule.html b/templates/home/schedule.html
new file mode 100644
index 0000000..c4ccbf5
--- /dev/null
+++ b/templates/home/schedule.html
@@ -0,0 +1,192 @@
+{% extends "partials/base.html" %}
+{% block css %}
+<style>
+ .anime-card::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 9.5rem;
+ height: 12.25rem;
+ background-position: center center;
+ background-size: cover;
+ background-repeat: no-repeat;
+ clip-path: polygon(100% 0, 100% 100%, 0 0);
+ z-index: 1;
+ filter: brightness(50%);
+ }
+ .sticker {
+ transform-origin: 0px 0px;
+ box-shadow: -2px 2px 4px var(--global-shadow);
+ transition: transform 0.2s;
+ display: flex;
+ align-items: flex-end;
+ justify-content: flex-start;
+ padding: 0.2rem;
+ }
+</style>
+<script>
+ function convertTime(unixTimestamp) {
+ const date = new Date(unixTimestamp * 1000);
+ return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true });
+ }
+
+ function convertDay(unixTimestamp) {
+ const date = new Date(unixTimestamp * 1000);
+ return date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' });
+ }
+</script>
+{% endblock css %}
+{% block content %}
+<div class="text-2xl font-bold mt-4">
+ <span id="todayTime"></span>
+</div>
+<section class="mt-4 flex flex-col lg:flex-row gap-2">
+ {% for anime in recently_aired %}
+ <style>
+ .anime-card-{{ forloop.counter }}::after {
+ background-image: url('{{ anime.media.bannerImage }}');
+ }
+ </style>
+ <a href="{% url "watch:watch_episode" anime.mediaId anime.episode %}" class="flex flex-1 bg-neutral-950 rounded pt-2 pb-2 pl-2 pr-8 relative overflow-hidden anime-card anime-card-{{ forloop.counter }}">
+ {% if forloop.counter == 1 %}
+ <div class="sticker text-pink-500 absolute top-1 -left-[0.35rem] w-10 h-8 rounded-b-lg bg-pink-300 border-2 border-pink-400 z-10 transform rotate-[-45deg]">
+ <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg>
+ </div>
+ {% else %}
+ <div class="sticker text-red-500 absolute top-1 -left-[0.35rem] w-10 h-8 rounded-b-lg bg-red-300 border-2 border-red-400 z-10 transform rotate-[-45deg]">
+ <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 192 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M176 432c0 44.112-35.888 80-80 80s-80-35.888-80-80 35.888-80 80-80 80 35.888 80 80zM25.26 25.199l13.6 272C39.499 309.972 50.041 320 62.83 320h66.34c12.789 0 23.331-10.028 23.97-22.801l13.6-272C167.425 11.49 156.496 0 142.77 0H49.23C35.504 0 24.575 11.49 25.26 25.199z"></path></svg>
+ </div>
+ {% endif %}
+ <div class="flex justify-between items-center w-full z-10">
+ <div class="flex flex-col gap-1 max-w-[calc(100%-8rem)]">
+ <span class="mt-4 truncate max-w-full overflow-hidden text-ellipsis whitespace-nowrap font-bold text-xl text-white">
+ {% if user.preferences.title_language == "english" and anime.media.title.english %}
+ {{ anime.media.title.english }}
+ {% elif user.preferences.title_language == "native" and anime.media.title.native %}
+ {{ anime.media.title.native }}
+ {% else %}
+ {{ anime.media.title.romaji }}
+ {% endif %}
+ </span>
+ <div class="flex flex-row gap-2 items-center text-sm text-gray-300">
+ <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M371.7 238l-176-107c-15.8-8.8-35.7 2.5-35.7 21v208c0 18.4 19.8 29.8 35.7 21l176-101c16.4-9.1 16.4-32.8 0-42zM504 256C504 119 393 8 256 8S8 119 8 256s111 248 248 248 248-111 248-248zm-448 0c0-110.5 89.5-200 200-200s200 89.5 200 200-89.5 200-200 200S56 366.5 56 256z"></path></svg>
+ <span>Episode {{ anime.episode }}</span>
+ </div>
+ <div class="flex flex-row gap-2 items-center text-sm text-gray-300">
+ <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm61.8-104.4l-84.9-61.7c-3.1-2.3-4.9-5.9-4.9-9.7V116c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v141.7l66.8 48.6c5.4 3.9 6.5 11.4 2.6 16.8L334.6 349c-3.9 5.3-11.4 6.5-16.8 2.6z"></path></svg>
+ <span>
+ {% if anime.timeUntilAiring > 0 %}
+ Airing
+ {% else %}
+ Aired
+ {% endif %}
+ at <script>document.write(convertTime({{ anime.airingAt }}))</script>
+ </span>
+ </div>
+ </div>
+ <img src="{{ anime.media.coverImage.medium }}" alt="{{ anime.media.title.romaji }}" class="rounded w-16 h-24" />
+ </div>
+ </a>
+ {% endfor %}
+ <style>
+ .anime-card-next-airing::after {
+ background-image: url('{{ next_airing.media.bannerImage }}');
+ }
+ </style>
+ <a href="{% url "watch:watch_episode" next_airing.mediaId next_airing.episode %}" class="flex flex-1 bg-neutral-950 rounded pt-2 pb-2 pl-2 pr-8 relative overflow-hidden anime-card anime-card-next-airing">
+ <div class="sticker text-orange-500 absolute top-1 -left-[0.35rem] w-10 h-8 rounded-b-lg bg-orange-300 border-2 border-orange-400 z-10 transform rotate-[-45deg]">
+ <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" height="1rem" width="1rem" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M0 0h24v24H0z"></path><path d="m23 12-2.44-2.78.34-3.68-3.61-.82-1.89-3.18L12 3 8.6 1.54 6.71 4.72l-3.61.81.34 3.68L1 12l2.44 2.78-.34 3.69 3.61.82 1.89 3.18L12 21l3.4 1.46 1.89-3.18 3.61-.82-.34-3.68L23 12zm-10 5h-2v-2h2v2zm0-4h-2V7h2v6z"></path></svg>
+ </div>
+ <div class="flex justify-between items-center w-full z-10">
+ <div class="flex flex-col gap-1 mr-2 max-w-[calc(100%-8rem)]">
+ <span class="truncate mt-4 max-w-full overflow-hidden text-ellipsis whitespace-nowrap font-bold text-xl text-white">
+ {% if user.preferences.title_language == "english" and next_airing.media.title.english %}
+ {{ next_airing.media.title.english }}
+ {% elif user.preferences.title_language == "native" and next_airing.media.title.native %}
+ {{ next_airing.media.title.native }}
+ {% else %}
+ {{ next_airing.media.title.romaji }}
+ {% endif %}
+ </span>
+ <div class="flex flex-row gap-2 items-center text-sm text-gray-300">
+ <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M371.7 238l-176-107c-15.8-8.8-35.7 2.5-35.7 21v208c0 18.4 19.8 29.8 35.7 21l176-101c16.4-9.1 16.4-32.8 0-42zM504 256C504 119 393 8 256 8S8 119 8 256s111 248 248 248 248-111 248-248zm-448 0c0-110.5 89.5-200 200-200s200 89.5 200 200-89.5 200-200 200S56 366.5 56 256z"></path></svg>
+ <span>Episode {{ next_airing.episode }}</span>
+ </div>
+ <div class="flex flex-row gap-2 items-center text-sm text-gray-300">
+ <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm61.8-104.4l-84.9-61.7c-3.1-2.3-4.9-5.9-4.9-9.7V116c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v141.7l66.8 48.6c5.4 3.9 6.5 11.4 2.6 16.8L334.6 349c-3.9 5.3-11.4 6.5-16.8 2.6z"></path></svg>
+ <span>
+ {% if next_airing.timeUntilAiring > 0 %}
+ Airing
+ {% else %}
+ Aired
+ {% endif %}
+ at <script>document.write(convertTime({{ next_airing.airingAt }}))</script>
+ </span>
+ </div>
+ </div>
+ <img src="{{ next_airing.media.coverImage.medium }}" alt="{{ next_airing.media.title.romaji }}" class="rounded w-16 h-24" />
+ </div>
+ </a>
+</section>
+<section class="mt-8 mx-auto flex justify-center px-4">
+ <section class="inline-flex w-full lg:max-w-fit sm:max-w-96 flex-row gap-4 rounded-full bg-white bg-opacity-10 overflow-x-auto">
+ {% for time in times %}
+ <a href="{% url "home:schedule" %}?start={{ time.start }}&end={{ time.end }}" class="flex flex-row items-center focus:outline-none gap-2 text-sm font-bold py-2 px-4 rounded-full {% if time.today %}bg-{{ user.preferences.accent_colour }}-600{% else %}{% endif %}">
+ <span>{{ time.day }}</span>
+ </a>
+ {% endfor %}
+ </section>
+</section>
+<h1 class="mt-8 text-2xl font-bold"><script>document.write(convertDay({{ start }}))</script></h1>
+{% for time, items in schedule.items %}
+<section class="my-4">
+ <h1 class="text-xl my-2 font-bold"><script>document.write(convertTime({{ time.timestamp }}))</script></h1>
+ <div class="flex flex-col lg:flex-row gap-2">
+ {% for anime in items %}
+ <a href="{% url "watch:watch_episode" anime.mediaId anime.episode %}" class="flex flex-none bg-neutral-950 rounded p-2 gap-4 items-center max-w-96 w-full {% if anime.timeUntilAiring < 0 %}border-2 border-{{ user.preferences.accent_colour }}-600{% endif %}">
+ <img src="{{ anime.media.coverImage.medium }}" alt="{{ anime.media.title.romaji }}" class="rounded w-16 h-24" />
+ <div class="flex flex-col gap-1 max-w-[calc(100%-8rem)]">
+ <span class="truncate max-w-full overflow-hidden text-ellipsis whitespace-nowrap font-bold text-xl text-white">
+ {% if user.preferences.title_language == "english" and anime.media.title.english %}
+ {{ anime.media.title.english }}
+ {% elif user.preferences.title_language == "native" and anime.media.title.native %}
+ {{ anime.media.title.native }}
+ {% else %}
+ {{ anime.media.title.romaji }}
+ {% endif %}
+ </span>
+ <div class="flex flex-row gap-2 items-center text-sm text-gray-300">
+ <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M371.7 238l-176-107c-15.8-8.8-35.7 2.5-35.7 21v208c0 18.4 19.8 29.8 35.7 21l176-101c16.4-9.1 16.4-32.8 0-42zM504 256C504 119 393 8 256 8S8 119 8 256s111 248 248 248 248-111 248-248zm-448 0c0-110.5 89.5-200 200-200s200 89.5 200 200-89.5 200-200 200S56 366.5 56 256z"></path></svg>
+ <span>Episode {{ anime.episode }}</span>
+ </div>
+ <div class="flex flex-row gap-2 items-center text-sm text-gray-300">
+ <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm61.8-104.4l-84.9-61.7c-3.1-2.3-4.9-5.9-4.9-9.7V116c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v141.7l66.8 48.6c5.4 3.9 6.5 11.4 2.6 16.8L334.6 349c-3.9 5.3-11.4 6.5-16.8 2.6z"></path></svg>
+ <span>
+ {% if anime.timeUntilAiring > 0 %}
+ Airing
+ {% else %}
+ Aired
+ {% endif %}
+ at <script>document.write(convertTime({{ anime.airingAt }}))</script>
+ </span>
+ </div>
+ </div>
+ </a>
+ {% endfor %}
+ </div>
+{% endfor %}
+</section>
+{% endblock content %}
+{% block scripts %}
+<script>
+ function updateTime() {
+ const today = new Date();
+ const time = today.toLocaleTimeString('en-US', { hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true });
+ document.getElementById('todayTime').innerText = time;
+ }
+
+ setInterval(updateTime, 1000);
+ updateTime();
+</script>
+{% endblock scripts %} \ No newline at end of file
diff --git a/templates/partials/navbar.html b/templates/partials/navbar.html
index 342175e..6c49fd2 100644
--- a/templates/partials/navbar.html
+++ b/templates/partials/navbar.html
@@ -81,8 +81,8 @@
</a>
<!-- Schedule icon -->
- <a href="#schedule" class="flex flex-col gap-2 items-center">
- {% if request.path == '/schedule/' %}
+ <a href="{% url "home:schedule" %}" class="flex flex-col gap-2 items-center">
+ {% if request.path == '/schedule' %}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
@@ -332,8 +332,8 @@
</a>
<!-- Schedule icon -->
- <a href="#schedule" class="flex flex-col gap-2 items-center">
- {% if request.path == '/schedule/' %}
+ <a href="{% url "home:schedule" %}" class="flex flex-col gap-2 items-center">
+ {% if request.path == '/schedule' %}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"