aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2024-12-21 00:46:45 -0500
committerBobby <[email protected]>2024-12-21 00:46:45 -0500
commitb453706835fcb5fe960375c7101074f3bb9c1c7a (patch)
tree34e3a2e9a6202c032c7703dc9adbb12f530aed70
parent70bd8ebf6e0de202cb3a4f52f39766e69f146053 (diff)
downloadthatcomputerscientist-b453706835fcb5fe960375c7101074f3bb9c1c7a.tar.xz
thatcomputerscientist-b453706835fcb5fe960375c7101074f3bb9c1c7a.zip
anime stream pages
-rw-r--r--apps/anime/__init__.py0
-rw-r--r--apps/anime/admin.py3
-rw-r--r--apps/anime/apps.py6
-rw-r--r--apps/anime/migrations/__init__.py0
-rw-r--r--apps/anime/models.py3
-rw-r--r--apps/anime/tests.py3
-rw-r--r--apps/anime/urls.py10
-rw-r--r--apps/anime/views.py210
-rw-r--r--apps/journals/urls.py1
-rw-r--r--apps/journals/views.py15
-rw-r--r--static/css/anime/anime.css378
-rw-r--r--static/images/anime/title_background.pngbin0 -> 2313653 bytes
-rw-r--r--static/js/shared/animeList.js45
-rw-r--r--templates/en/anime/anime.html12
-rw-r--r--templates/en/anime/home.html46
-rw-r--r--templates/en/anime/search.html78
-rw-r--r--templates/en/journals/single.html41
-rw-r--r--templates/partials/_anime_list.html62
-rw-r--r--templates/shared/left_sidebar.html4
-rw-r--r--thatcomputerscientist/settings.py2
-rw-r--r--thatcomputerscientist/templatetags/color_to_rgb.py21
-rw-r--r--thatcomputerscientist/templatetags/pagination.py43
-rw-r--r--thatcomputerscientist/urls.py1
23 files changed, 958 insertions, 26 deletions
diff --git a/apps/anime/__init__.py b/apps/anime/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/apps/anime/__init__.py
diff --git a/apps/anime/admin.py b/apps/anime/admin.py
new file mode 100644
index 00000000..8c38f3f3
--- /dev/null
+++ b/apps/anime/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/apps/anime/apps.py b/apps/anime/apps.py
new file mode 100644
index 00000000..9f6afd41
--- /dev/null
+++ b/apps/anime/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class AnimeConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "apps.anime"
diff --git a/apps/anime/migrations/__init__.py b/apps/anime/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/apps/anime/migrations/__init__.py
diff --git a/apps/anime/models.py b/apps/anime/models.py
new file mode 100644
index 00000000..71a83623
--- /dev/null
+++ b/apps/anime/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/apps/anime/tests.py b/apps/anime/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/apps/anime/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/apps/anime/urls.py b/apps/anime/urls.py
new file mode 100644
index 00000000..d9b7b810
--- /dev/null
+++ b/apps/anime/urls.py
@@ -0,0 +1,10 @@
+from django.urls import path, re_path
+
+from . import views
+
+app_name = "anime"
+urlpatterns = [
+ path("", views.home, name="home"),
+ path("search/", views.search, name="search"),
+ re_path(r"^(?P<anime_id>\d+)(?:\.(?P<e>\d+))?/$", views.anime, name="anime"),
+]
diff --git a/apps/anime/views.py b/apps/anime/views.py
new file mode 100644
index 00000000..6b976391
--- /dev/null
+++ b/apps/anime/views.py
@@ -0,0 +1,210 @@
+import os
+from django.urls import reverse
+import requests
+from django.shortcuts import redirect, render
+
+from thatcomputerscientist.utils import i18npatterns
+
+genres = [
+ "Action",
+ "Adventure",
+ "Cars",
+ "Comedy",
+ "Drama",
+ "Fantasy",
+ "Horror",
+ "Mahou Shoujo",
+ "Mecha",
+ "Music",
+ "Mystery",
+ "Psychological",
+ "Romance",
+ "Sci-Fi",
+ "Slice of Life",
+ "Sports",
+ "Supernatural",
+ "Thriller",
+]
+
+sortings = [
+ {"name": "Popularity", "value": "popularity"},
+ {"name": "Trending", "value": "trending"},
+ {"name": "Start Date", "value": "start_date"},
+ {"name": "End Date", "value": "end_date"},
+ {"name": "Score", "value": "score"},
+ {"name": "Favourites", "value": "favourites"},
+ {"name": "Title", "value": "title"},
+]
+
+ANIME_PROVIDER_MAP = {}
+
+
+def get_anime(anime_id, dub=False):
+ if anime_id in ANIME_PROVIDER_MAP:
+ provider = ANIME_PROVIDER_MAP[anime_id]
+ else:
+ provider = "zoro"
+
+ dub = "true" if dub else "false"
+ base_url = f"{os.getenv('CONSUMET_URL')}/meta/anilist/info/{anime_id}"
+ params = {"provider": provider, "dub": dub}
+
+ response = requests.get(base_url, params=params)
+ data = response.json()
+ if (
+ (not data.get("episodes") or len(data.get("episodes")) == 0)
+ and provider == "zoro"
+ and data.get("status") != "Not yet aired"
+ ):
+ ANIME_PROVIDER_MAP[anime_id] = "gogoanime"
+ return get_anime(anime_id)
+ else:
+ ANIME_PROVIDER_MAP[anime_id] = provider
+ return data
+
+
+def sort_mapper(sort_by, order):
+ sort_mappings = {
+ "popularity": "POPULARITY",
+ "trending": "TRENDING",
+ "start_date": "START_DATE",
+ "end_date": "END_DATE",
+ "score": "SCORE",
+ "favourites": "FAVOURITES",
+ "title": "TITLE_ROMAJI",
+ }
+
+ if sort_by not in sort_mappings or order not in ["asc", "desc"]:
+ return None
+
+ sort_value = sort_mappings[sort_by]
+ order_suffix = "" if order == "asc" else "_DESC"
+
+ return f"{sort_value}{order_suffix}"
+
+
+def bracketed_string(string):
+ return f'["{string}"]'
+
+
+def anime_results(
+ sort="trending", order="desc", genre="", query="", page=1, per_page=12, status=""
+):
+ supported_status = [
+ "releasing",
+ "not_yet_released",
+ "cancelled",
+ "finished",
+ "hiatus",
+ ]
+
+ if status and status not in supported_status:
+ status = ""
+
+ base_url = f"{os.getenv('CONSUMET_URL')}/meta/anilist/advanced-search"
+ params = {"page": page, "perPage": per_page, "type": "ANIME"}
+
+ if query:
+ params["query"] = query
+
+ if sort and order:
+ sort_value = sort_mapper(sort, order)
+ if sort_value:
+ params["sort"] = bracketed_string(sort_value)
+
+ if genre:
+ params["genres"] = bracketed_string(genre)
+
+ if status:
+ params["status"] = status.upper()
+
+ response = requests.get(base_url, params=params)
+ return response.json()
+
+
+def home(request):
+ META = {
+ "title": "Anime: Home",
+ }
+ LANGUAGE_CODE = i18npatterns(request.LANGUAGE_CODE)
+ request.meta.update(META)
+
+ context = {
+ "genres": genres,
+ "sortings": sortings,
+ "trending_anime": anime_results(sort="trending", per_page=8),
+ "popular_anime": anime_results(sort="popularity", per_page=8),
+ "top_anime": anime_results(sort="score", per_page=8),
+ "top_airing_anime": anime_results(
+ sort="popularity", status="releasing", per_page=8
+ ),
+ }
+
+ return render(request, f"{LANGUAGE_CODE}/anime/home.html", context)
+
+
+def search(request):
+ # Get search parameters
+ query = request.GET.get("q", "")
+ genre = request.GET.get("genre", "")
+ sort = request.GET.get("sort", "popularity")
+ order = request.GET.get("order", "desc")
+ page = int(request.GET.get("page", 1))
+
+ META = {
+ "title": f"Anime: Search",
+ }
+ if query:
+ META["title"] = f"Anime: Search Results for {query}"
+ LANGUAGE_CODE = i18npatterns(request.LANGUAGE_CODE)
+ request.meta.update(META)
+
+ # Get search results
+ search_results = anime_results(
+ query=query, genre=genre, sort=sort, order=order, page=page, per_page=32
+ )
+
+ context = {
+ "genres": genres,
+ "sortings": sortings,
+ "search_results": search_results,
+ "current_page": page,
+ "total_pages": search_results.get("totalPages", 1),
+ "total_results": search_results.get("totalResults", 0),
+ }
+
+ return render(request, f"{LANGUAGE_CODE}/anime/search.html", context)
+
+
+def anime(request, anime_id, e=None):
+ dub = request.COOKIES.get("anime_dub", False)
+ anime_data = get_anime(anime_id, dub)
+
+ if len(anime_data.get("episodes")) > 0:
+ episode_numbers = [
+ int(episode.get("number")) for episode in anime_data.get("episodes")
+ ]
+
+ if not e:
+ e = episode_numbers[0]
+ return redirect(
+ reverse("anime:anime", kwargs={"anime_id": anime_id, "e": e})
+ )
+
+ e = int(e)
+ if e not in episode_numbers:
+ return redirect(
+ reverse("anime:anime", kwargs={"anime_id": anime_id, "e": 1})
+ )
+
+ META = {
+ "title": f"Anime: {anime_data.get('title').get('romaji')}",
+ }
+ LANGUAGE_CODE = i18npatterns(request.LANGUAGE_CODE)
+ request.meta.update(META)
+
+ context = {
+ "anime": anime_data,
+ }
+
+ return render(request, f"{LANGUAGE_CODE}/anime/anime.html", context)
diff --git a/apps/journals/urls.py b/apps/journals/urls.py
index b4e7db01..bc7155c3 100644
--- a/apps/journals/urls.py
+++ b/apps/journals/urls.py
@@ -4,5 +4,6 @@ from . import views
app_name = "journal"
urlpatterns = [
+ path("", views.journal_of_random_thoughts, name="journal_of_random_thoughts"),
path("<slug:slug>/", views.single_journal, name="single"),
]
diff --git a/apps/journals/views.py b/apps/journals/views.py
index 9c3f99f7..070c20b0 100644
--- a/apps/journals/views.py
+++ b/apps/journals/views.py
@@ -3,7 +3,20 @@ from apps.journals.models import Journal
from thatcomputerscientist.utils import i18npatterns
-# Create your views here.
+def journal_of_random_thoughts(request):
+ META = {
+ "title": "Journal: Journal of Random Thoughts",
+ }
+ LANGUAGE_CODE = i18npatterns(request.LANGUAGE_CODE)
+ request.meta.update(META)
+ slug = "journal-of-random-thoughts"
+ journal = Journal.objects.get(slug=slug)
+ context = {
+ "journal": journal,
+ }
+ return render(request, f"{LANGUAGE_CODE}/journals/single.html", context)
+
+
def single_journal(request, slug):
try:
journal = Journal.objects.get(slug=slug)
diff --git a/static/css/anime/anime.css b/static/css/anime/anime.css
new file mode 100644
index 00000000..a68e0ee8
--- /dev/null
+++ b/static/css/anime/anime.css
@@ -0,0 +1,378 @@
+.anime-list-section {
+ margin: 20px 0;
+}
+
+.anime-list-section .section-title {
+ font-size: 18px;
+ margin-bottom: 15px;
+ text-shadow: 0 0 10px rgba(126, 232, 199, 0.4);
+}
+
+/* Anime Grid */
+.anime-grid {
+ width: 780px;
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 15px;
+}
+
+.anime-grid a:hover {
+ text-decoration: none;
+}
+
+/* Adjust card to maintain consistent height */
+.anime-card {
+ position: relative;
+ width: 180px;
+ display: flex;
+ flex-direction: column;
+ min-height: 326px;
+}
+
+/* Poster container */
+.anime-poster {
+ width: 180px;
+ height: 256px;
+ overflow: hidden;
+ border-radius: 4px;
+ border: 1px solid rgba(141, 141, 255, 0.2);
+ box-shadow: 0 0 10px rgba(141, 141, 255, 0.1);
+ transition: all 0.2s ease;
+}
+
+.anime-card:hover .anime-poster {
+ border-color: rgba(223, 35, 196, 0.4);
+ box-shadow: 0 0 15px rgba(223, 35, 196, 0.2);
+ transform: translateY(-2px);
+}
+
+.anime-poster img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ image-rendering: -webkit-optimize-contrast;
+}
+
+.anime-basic-info {
+ padding: 10px 0;
+ min-height: 60px;
+ display: flex;
+ flex-direction: column;
+}
+
+.anime-title {
+ font-size: 14px;
+ margin: 0 0 8px 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: var(--anime-color, #8d8dff);
+ text-shadow: 0 0 10px rgba(var(--anime-color-rgb, 141, 141, 255), 0.4);
+ transition: color 0.2s ease, text-shadow 0.2s ease;
+}
+
+.anime-card:hover .anime-title {
+ filter: brightness(1.2);
+}
+
+.anime-meta {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 12px;
+ color: rgba(141, 141, 255, 0.8);
+ margin-top: auto;
+}
+
+.anime-status {
+ padding: 2px 6px;
+ background: rgba(98, 55, 149, 0.3);
+ border: 1px solid rgba(141, 141, 255, 0.2);
+ border-radius: 2px;
+}
+
+/* Hover card */
+.anime-hover-card {
+ position: fixed;
+ width: 400px;
+ background: rgba(13, 20, 28, 0.95);
+ border: 1px solid #8d8dff;
+ border-radius: 4px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
+ opacity: 0;
+ visibility: hidden;
+ pointer-events: none;
+ z-index: 1000;
+ transition: opacity 0.15s ease;
+}
+
+.hover-card-cover {
+ height: 150px;
+ position: relative;
+ overflow: hidden;
+}
+
+.hover-card-cover img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ opacity: 0.7;
+}
+
+.hover-card-color {
+ width: 100%;
+ height: 100%;
+ opacity: 0.3;
+}
+
+.hover-card-title-area {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 40px 15px 15px;
+ background: linear-gradient(to top, rgba(13, 20, 28, 0.9), rgba(13, 20, 28, 0.7) 50%, transparent);
+}
+
+.hover-card-title-area h3 {
+ margin: 0;
+ font-size: 18px;
+ line-height: 1.4;
+ color: var(--anime-color, #8d8dff);
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5),
+ 0 0 10px rgba(var(--anime-color-rgb, 141, 141, 255), 0.4);
+}
+
+.hover-card-content {
+ padding: 15px;
+ display: grid;
+ grid-template-columns: 100px 1fr;
+ gap: 15px;
+}
+
+.hover-card-poster {
+ width: 100px;
+ height: 142px;
+ border-radius: 4px;
+ overflow: hidden;
+ border: 1px solid #8d8dff;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+}
+
+.hover-card-poster img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.hover-card-details {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+.hover-card-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.hover-card-meta span {
+ padding: 2px 6px;
+ background: rgba(98, 55, 149, 0.3);
+ border: 1px solid rgba(141, 141, 255, 0.2);
+ border-radius: 2px;
+}
+
+.hover-card-description {
+ font-size: 13px;
+ color: rgba(141, 141, 255, 0.8);
+ line-height: 1.5;
+}
+
+.hover-card-genres {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.genre-tag {
+ font-size: 11px;
+ padding: 2px 6px;
+ background: rgba(173, 128, 236, 0.15);
+ border: 1px solid rgba(141, 141, 255, 0.2);
+ border-radius: 2px;
+ color: #8d8dff;
+}
+
+/* Search form styles */
+.a-title-banner {
+ width: 780px;
+ height: 195px;
+ background: url(../../images/anime/title_background.png) no-repeat;
+ background-size: 780px 195px;
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.overlay-bottom-radial {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 780px;
+ height: 195px;
+ background: radial-gradient(ellipse 600px 195px at center bottom, rgba(13, 20, 28, 0.95) 0%, rgba(19, 26, 44, 0.8) 50%, transparent 100%);
+ z-index: 1;
+}
+
+.a-title-banner-content {
+ position: relative;
+ z-index: 2;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+}
+
+.a-title-banner-content h1 {
+ color: #7ee8c7;
+ text-shadow: 0 0 8px rgba(126, 232, 199, 0.4);
+ font-family: 'SweetFairy', sans-serif;
+}
+
+.a-title-banner-content form {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ color: #7ee8c7;
+}
+
+.a-title-banner-content form input,
+.a-title-banner-content form select {
+ background: rgba(13, 20, 28, 0.85);
+ padding: 4px 8px;
+ border: solid 1px #8d8dff;
+ border-radius: 2px;
+ color: #7ee8c7;
+}
+
+.a-title-banner-content form input:focus,
+.a-title-banner-content form select:focus {
+ border-color: #df23c4;
+ outline: none;
+ box-shadow: 0 0 4px rgba(223, 35, 196, 0.2);
+}
+
+.a-title-banner-content form button[type="submit"] {
+ background: rgba(68, 68, 177, 0.85);
+ padding: 5px 10px;
+ border: solid 1px #8d8dff;
+ border-radius: 2px;
+ color: #fff;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+.a-title-banner-content form button[type="submit"]:hover {
+ background: #df23c4;
+ border-color: #df23c4;
+ box-shadow: 0 0 8px rgba(223, 35, 196, 0.2);
+}
+
+.order-toggle {
+ display: inline-flex;
+ background: rgba(13, 20, 28, 0.85);
+ border: 1px solid #8d8dff;
+ border-radius: 2px;
+ overflow: hidden;
+}
+
+input[type="radio"][name="order"] {
+ display: none;
+}
+
+input[type="radio"][name="order"]+label {
+ padding: 4px 8px;
+ cursor: pointer;
+ color: #7ee8c7;
+ border-right: 1px solid #8d8dff;
+}
+
+input[type="radio"][name="order"]+label:last-of-type {
+ border-right: none;
+}
+
+input[type="radio"][name="order"]:checked+label {
+ background: rgba(68, 68, 177, 0.85);
+ color: #7ee8c7;
+ box-shadow: inset 0 0 4px rgba(141, 141, 255, 0.2);
+}
+
+input[type="radio"][name="order"]+label:hover {
+ background: rgba(223, 35, 196, 0.3);
+}
+
+/* Search Results Info */
+.search-results-info {
+ margin: 20px 0;
+ padding: 10px;
+ background: rgba(13, 20, 28, 0.85);
+ border: 1px solid #8d8dff;
+ border-radius: 4px;
+}
+
+.search-summary {
+ color: #7ee8c7;
+ font-size: 14px;
+}
+
+.search-summary .result-count {
+ color: #df23c4;
+ font-weight: bold;
+}
+
+.search-summary .search-term,
+.search-summary .search-genre {
+ color: #8d8dff;
+ font-weight: bold;
+}
+
+/* Pagination */
+.pagination {
+ margin: 20px 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+}
+
+.page-link {
+ padding: 6px 12px;
+ background: rgba(13, 20, 28, 0.85);
+ border: 1px solid #8d8dff;
+ border-radius: 4px;
+ color: #7ee8c7;
+ text-decoration: none;
+ transition: all 0.2s ease;
+}
+
+.page-link:hover {
+ background: rgba(223, 35, 196, 0.2);
+ border-color: #df23c4;
+ color: #df23c4;
+ text-decoration: none;
+}
+
+.page-link.active {
+ background: #8d8dff;
+ color: #000;
+ border-color: #8d8dff;
+}
+
+.page-ellipsis {
+ color: #7ee8c7;
+ padding: 6px 12px;
+} \ No newline at end of file
diff --git a/static/images/anime/title_background.png b/static/images/anime/title_background.png
new file mode 100644
index 00000000..da494a07
--- /dev/null
+++ b/static/images/anime/title_background.png
Binary files differ
diff --git a/static/js/shared/animeList.js b/static/js/shared/animeList.js
new file mode 100644
index 00000000..2d2526b0
--- /dev/null
+++ b/static/js/shared/animeList.js
@@ -0,0 +1,45 @@
+document.querySelectorAll('.anime-card').forEach(card => {
+ const hoverCard = card.querySelector('.anime-hover-card');
+
+ card.addEventListener('mouseenter', (e) => {
+ positionHoverCard(e, hoverCard);
+ hoverCard.style.opacity = '1';
+ hoverCard.style.visibility = 'visible';
+ });
+
+ card.addEventListener('mousemove', (e) => {
+ positionHoverCard(e, hoverCard);
+ });
+
+ card.addEventListener('mouseleave', () => {
+ hoverCard.style.opacity = '0';
+ hoverCard.style.visibility = 'hidden';
+ });
+});
+
+function positionHoverCard(e, hoverCard) {
+ const mouseX = e.clientX;
+ const mouseY = e.clientY;
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+ const cardWidth = 400;
+ const cardHeight = hoverCard.offsetHeight;
+
+ // Calculate position, keeping card within viewport
+ let left = mouseX + 20; // 20px offset from cursor
+ let top = mouseY + 20;
+
+ // Adjust if card would overflow right side
+ if (left + cardWidth > viewportWidth) {
+ left = mouseX - cardWidth - 20;
+ }
+
+ // Adjust if card would overflow bottom
+ if (top + cardHeight > viewportHeight) {
+ top = mouseY - cardHeight - 20;
+ }
+
+ // Apply position
+ hoverCard.style.left = `${left}px`;
+ hoverCard.style.top = `${top}px`;
+} \ No newline at end of file
diff --git a/templates/en/anime/anime.html b/templates/en/anime/anime.html
new file mode 100644
index 00000000..c26a8f5c
--- /dev/null
+++ b/templates/en/anime/anime.html
@@ -0,0 +1,12 @@
+{% extends 'shared/base.html' %}
+{% load static %}
+{% block head %}
+<link rel="stylesheet" href="{% static 'css/anime/anime.css' %}">
+{% endblock head %}
+{% block content %}
+{{ anime }}
+
+{% endblock content %}
+
+{% block scripts %}
+{% endblock scripts %}
diff --git a/templates/en/anime/home.html b/templates/en/anime/home.html
new file mode 100644
index 00000000..aba7c856
--- /dev/null
+++ b/templates/en/anime/home.html
@@ -0,0 +1,46 @@
+{% extends 'shared/base.html' %}
+{% load static %}
+{% block head %}
+<link rel="stylesheet" href="{% static 'css/anime/anime.css' %}">
+{% endblock head %}
+{% block content %}
+<div class="a-title-banner">
+ <div class="overlay-bottom-radial"></div>
+ <div class="a-title-banner-content">
+ <h1>Anime</h1>
+ <form method="GET" action="{% url 'anime:search' %}">
+ <label for="q">Keywords:</label>
+ <input type="text" name="q" placeholder="Search anime..." value="{{ request.GET.q }}">
+ <label for="genre">Genre:</label>
+ <select name="genre" id="genre">
+ <option value="">All</option>
+ {% for genre in genres %}
+ <option value="{{ genre }}" {% if request.GET.genre == genre %}selected{% endif %}>{{ genre }}</option>
+ {% endfor %}
+ </select>
+ <label for="sort">Sort by:</label>
+ <select name="sort" id="sort">
+ {% for sortOption in sortings %}
+ <option value="{{ sortOption.value }}" {% if request.GET.sort == sortOption.value %}selected{% endif %}>{{ sortOption.name }}</option>
+ {% endfor %}
+ </select>
+ <div class="order-toggle">
+ <input type="radio" name="order" value="asc" id="asc" {% if request.GET.order == 'asc' %}checked{% endif %}>
+ <label for="asc">↑</label>
+ <input type="radio" name="order" value="desc" id="desc" {% if request.GET.order == 'desc' %}checked{% elif not request.GET.order %}checked{% endif %}>
+ <label for="desc">↓</label>
+ </div>
+ <button type="submit">Search</button>
+ </form>
+ </div>
+</div>
+{% include 'partials/_anime_list.html' with anime_list=trending_anime title="Trending Anime" %}
+{% include 'partials/_anime_list.html' with anime_list=popular_anime title="Popular Anime" %}
+{% include 'partials/_anime_list.html' with anime_list=top_anime title="Top Rated Anime" %}
+{% include 'partials/_anime_list.html' with anime_list=top_airing_anime title="Top Airing Anime" %}
+
+{% endblock content %}
+
+{% block scripts %}
+<script src="{% static 'js/shared/animeList.js' %}"></script>
+{% endblock scripts %}
diff --git a/templates/en/anime/search.html b/templates/en/anime/search.html
new file mode 100644
index 00000000..7fcc5e90
--- /dev/null
+++ b/templates/en/anime/search.html
@@ -0,0 +1,78 @@
+{% extends 'shared/base.html' %}
+{% load static %}
+{% load pagination %}
+{% block head %}
+<link rel="stylesheet" href="{% static 'css/anime/anime.css' %}">
+{% endblock head %}
+
+{% block content %}
+<div class="a-title-banner">
+ <div class="overlay-bottom-radial"></div>
+ <div class="a-title-banner-content">
+ <h1>Search Anime</h1>
+ <form method="GET">
+ <label for="q">Keywords:</label>
+ <input type="text" name="q" placeholder="Search anime..." value="{{ request.GET.q }}">
+ <label for="genre">Genre:</label>
+ <select name="genre" id="genre">
+ <option value="">All</option>
+ {% for genre in genres %}
+ <option value="{{ genre }}" {% if request.GET.genre == genre %}selected{% endif %}>{{ genre }}</option>
+ {% endfor %}
+ </select>
+ <label for="sort">Sort by:</label>
+ <select name="sort" id="sort">
+ {% for sortOption in sortings %}
+ <option value="{{ sortOption.value }}" {% if request.GET.sort == sortOption.value %}selected{% endif %}>{{ sortOption.name }}</option>
+ {% endfor %}
+ </select>
+ <div class="order-toggle">
+ <input type="radio" name="order" value="asc" id="asc" {% if request.GET.order == 'asc' %}checked{% endif %}>
+ <label for="asc">↑</label>
+ <input type="radio" name="order" value="desc" id="desc" {% if request.GET.order == 'desc' %}checked{% endif %}>
+ <label for="desc">↓</label>
+ </div>
+ <button type="submit">Search</button>
+ </form>
+ </div>
+</div>
+
+<div class="search-results-info">
+ <div class="search-summary">
+ Found <span class="result-count">{{ total_results }}</span> results
+ {% if request.GET.q %} for "<span class="search-term">{{ request.GET.q }}</span>"{% endif %}
+ {% if request.GET.genre %} in <span class="search-genre">{{ request.GET.genre }}</span>{% endif %}
+ </div>
+</div>
+
+{% include 'partials/_anime_list.html' with anime_list=search_results %}
+
+{% if total_pages > 1 %}
+<div class="pagination">
+ {% if current_page > 1 %}
+ {% with current_page|add:"-1" as prev_page %}
+ <a href="{% url 'anime:search' %}?{{ request.GET|set_page_param:prev_page }}" class="page-link">&lt;</a>
+ {% endwith %}
+ {% endif %}
+
+ {% for p in total_pages|get_page_range:current_page %}
+ {% if p == '...' %}
+ <span class="page-ellipsis">...</span>
+ {% else %}
+ <a href="{% url 'anime:search' %}?{{ request.GET|set_page_param:p }}"
+ class="page-link {% if p == current_page %}active{% endif %}">{{ p }}</a>
+ {% endif %}
+ {% endfor %}
+
+ {% if current_page < total_pages %}
+ {% with current_page|add:"1" as next_page %}
+ <a href="{% url 'anime:search' %}?{{ request.GET|set_page_param:next_page }}" class="page-link">&gt;</a>
+ {% endwith %}
+ {% endif %}
+</div>
+{% endif %}
+
+{% endblock content %}
+{% block scripts %}
+<script src="{% static 'js/shared/animeList.js' %}"></script>
+{% endblock scripts %} \ No newline at end of file
diff --git a/templates/en/journals/single.html b/templates/en/journals/single.html
index e538949f..084fea44 100644
--- a/templates/en/journals/single.html
+++ b/templates/en/journals/single.html
@@ -25,7 +25,17 @@
.journal-stats-area {
background-color: #f4f1e90f;
- border-radius: 8px;
+ padding: 8px 0px;
+ }
+
+ .journal-stats-area:first-child {
+ border-top-left-radius: 8px;
+ border-top-right-radius: 8px;
+ }
+
+ .journal-stats-area:last-child {
+ border-bottom-left-radius: 8px;
+ border-bottom-right-radius: 8px;
}
.journal-stats-area h3 {
@@ -36,7 +46,7 @@
.journal-stats-profile-image {
width: 128px;
height: 128px;
- margin: 4px auto;
+ margin: 4px auto 8px auto;
display: block;
}
@@ -56,11 +66,6 @@
font-weight: bold;
}
- .journal-stats:last-child {
- border-bottom-left-radius: 8px;
- border-bottom-right-radius: 8px;
- }
-
.journal-entry {
background-color: #f4f1e90f;
border-radius: 8px;
@@ -92,23 +97,21 @@
<div class="journal-sidebar">
<div class="journal-stats-area">
{% with journal.owner.userprofile_set.first as journal_holder_profile %}
- <h3>Journal Holder's Profile</h3>
<img src="{% static 'images/avatars/' %}{{ journal_holder_profile.avatar_url }}.gif" class="journal-stats-profile-image" alt="{{ journal.owner.first_name }}'s Avatar">
<div class="journal-stats">
- <p class="stat-name">Profile</p>
+ <p class="stat-name">Author</p>
<p class="stat-value">{{ journal.owner.first_name }} {{ journal.owner.last_name }}</p>
</div>
<div class="journal-stats">
<p class="stat-name">Bio</p>
<p class="stat-value">{{ journal_holder_profile.bio|linebreaksbr }}</p>
</div>
+ {% endwith %}
+ </div>
+ <div class="journal-stats-area">
<div class="journal-stats">
- <p class="stat-name">XP</p>
- <p class="stat-value">{{ journal_holder_profile.experience }} / 1000</p>
- </div>
- <div class="journal-stats">
- <p class="stat-name">Level</p>
- <p class="stat-value">{{ journal_holder_profile.level }}</p>
+ <p class="stat-name">Journal</p>
+ <p class="stat-value"><a href="{% url "journal:single" journal.slug %}">{{ journal.name }}</a></p>
</div>
<div class="journal-stats">
<p class="stat-name">Journal Entries</p>
@@ -116,17 +119,11 @@
</div>
<div class="journal-stats">
<p class="stat-name">Journal Streak</p>
- <p class="stat-value">{{ journal_holder_profile.journal_streak }} days</p>
+ <p class="stat-value">0 days</p>
</div>
- <div class="journal-stats">
- <p class="stat-name">Weblog Posts</p>
- <p class="stat-value">{{ journal_holder_profile.weblogs_created }}</p>
- </div>
- {% endwith %}
</div>
</div>
<div class="journal-entries">
- <p>{{ journal.entries.all|length }} entries</p>
{% for entry in journal.entries.all %}
<div class="journal-entry">
<h2 class="journal-entry-title">{{ entry.title }}</h2>
diff --git a/templates/partials/_anime_list.html b/templates/partials/_anime_list.html
new file mode 100644
index 00000000..c2530f34
--- /dev/null
+++ b/templates/partials/_anime_list.html
@@ -0,0 +1,62 @@
+{% load color_to_rgb %}
+<div class="anime-list-section">
+ {% if title %}
+ <h2 class="section-title">{{ title }}</h2>
+ {% endif %}
+ <div class="anime-grid">
+ {% for anime in anime_list.results %}
+ <a href="{% url "anime:anime" anime.id %}" class="anime-card" style="--anime-color: {{ anime.color|default:'#8d8dff' }};--anime-color-rgb: {{ anime.color|default:'141, 141, 255'|color_to_rgb }};">
+ <div class="anime-poster">
+ <img src="{{ anime.image }}" alt="{{ anime.title.english }}">
+ </div>
+ <div class="anime-basic-info">
+ <h3 class="anime-title" style="color:{{anime.color}}">{{ anime.title.english|default:anime.title.romaji }}</h3>
+ <div class="anime-meta">
+ <span class="anime-status">{{ anime.status }}</span>
+ {% if anime.totalEpisodes %}
+ <span>{{ anime.totalEpisodes }} eps</span>
+ {% endif %}
+ </div>
+ </div>
+
+ <div class="anime-hover-card">
+ <div class="hover-card-cover">
+ {% if anime.cover %}
+ <img src="{{ anime.cover }}" alt="Cover">
+ {% else %}
+ <div class="hover-card-color" style="background-color: {{ anime.color|default:'#1a1f29' }}"></div>
+ {% endif %}
+ <div class="hover-card-title-area">
+ <h3>{{ anime.title.english|default:anime.title.romaji }}</h3>
+ </div>
+ </div>
+ <div class="hover-card-content">
+ <div class="hover-card-poster">
+ <img src="{{ anime.image }}" alt="{{ anime.title.english }}">
+ </div>
+ <div class="hover-card-details">
+ <div class="hover-card-meta">
+ <span>{{ anime.type }}</span>
+ <span>{{ anime.status }}</span>
+ {% if anime.totalEpisodes %}
+ <span>{{ anime.totalEpisodes }} Episodes</span>
+ {% endif %}
+ {% if anime.rating %}
+ <span>★ {{ anime.rating }}%</span>
+ {% endif %}
+ </div>
+ <div class="hover-card-description">
+ {{ anime.description|truncatewords:50 }}
+ </div>
+ <div class="hover-card-genres">
+ {% for genre in anime.genres %}
+ <span class="genre-tag">{{ genre }}</span>
+ {% endfor %}
+ </div>
+ </div>
+ </div>
+ </div>
+ </a>
+ {% endfor %}
+ </div>
+</div> \ No newline at end of file
diff --git a/templates/shared/left_sidebar.html b/templates/shared/left_sidebar.html
index ee667db2..a0a546ca 100644
--- a/templates/shared/left_sidebar.html
+++ b/templates/shared/left_sidebar.html
@@ -63,7 +63,7 @@
</div>
<div class="navigation-item">
<img src="{% static 'images/core/icons/journalofrandomthoughts.png' %}" alt="Journal of Random Thoughts Icon" />
- <a href="#journalofrandomthoughts">{% if request.LANGUAGE_CODE == 'ja' %}ランダム思考のジャーナル{% else %}Journal of Random Thoughts{% endif %}</a>
+ <a href="{% url "journal:journal_of_random_thoughts" %}">{% if request.LANGUAGE_CODE == 'ja' %}ランダム思考のジャーナル{% else %}Journal of Random Thoughts{% endif %}</a>
</div>
<div class="navigation-item">
<img src="{% static 'images/core/icons/weblog.gif' %}" alt="Weblog Icon" />
@@ -133,7 +133,7 @@
</div>
<div class="navigation-item">
<img src="{% static 'images/core/icons/anime.png' %}" alt="Anime Icon" />
- <a href="#anime">{% if request.LANGUAGE_CODE == 'ja' %}アニメ配信{% else %}Anime Streams{% endif %}</a>
+ <a href="{% url "anime:home" %}">{% if request.LANGUAGE_CODE == 'ja' %}アニメ配信{% else %}Anime Streams{% endif %}</a>
</div>
<div class="navigation-item">
<img src="{% static 'images/core/icons/matrix.png' %}" alt="The Matrix Icon" />
diff --git a/thatcomputerscientist/settings.py b/thatcomputerscientist/settings.py
index 726415a3..97c05169 100644
--- a/thatcomputerscientist/settings.py
+++ b/thatcomputerscientist/settings.py
@@ -91,7 +91,7 @@ INSTALLED_APPS = [
]
SITE_ID = 1
-APPEND_SLASH = False
+APPEND_SLASH = True
# HAYSTACK_CONNECTIONS = {
# "default": {
# "ENGINE": "haystack.backends.simple_backend.SimpleEngine",
diff --git a/thatcomputerscientist/templatetags/color_to_rgb.py b/thatcomputerscientist/templatetags/color_to_rgb.py
new file mode 100644
index 00000000..ddf14b54
--- /dev/null
+++ b/thatcomputerscientist/templatetags/color_to_rgb.py
@@ -0,0 +1,21 @@
+from django import template
+from django.template.defaultfilters import stringfilter
+
+register = template.Library()
+
+
+@stringfilter
+def color_to_rgb(value):
+ """Convert hex color to RGB values"""
+ # Remove hash if present
+ value = value.lstrip("#")
+ # Handle both 3 and 6 character hex
+ if len(value) == 3:
+ value = "".join(c + c for c in value)
+ if len(value) == 6:
+ r = int(value[:2], 16)
+ g = int(value[2:4], 16)
+ b = int(value[4:6], 16)
+ return f"{r}, {g}, {b}"
+ return "141, 141, 255" # Default fallback
diff --git a/thatcomputerscientist/templatetags/pagination.py b/thatcomputerscientist/templatetags/pagination.py
new file mode 100644
index 00000000..76dd19b4
--- /dev/null
+++ b/thatcomputerscientist/templatetags/pagination.py
@@ -0,0 +1,43 @@
+from django import template
+from django.template.defaultfilters import stringfilter
+
+register = template.Library()
+
+
+def set_page_param(querydict, page_number):
+ """
+ Update page parameter while preserving all other query parameters
+ Usage: {{ request.GET|set_page_param:2 }}
+ """
+ params = querydict.copy()
+ params["page"] = str(page_number)
+ return params.urlencode()
+
+
+def get_page_range(total_pages, current_page):
+ """Generate pagination range with ellipsis"""
+ current_page = int(current_page)
+ total_pages = int(total_pages)
+
+ if total_pages <= 7:
+ return range(1, total_pages + 1)
+
+ pages = []
+ if current_page <= 4:
+ pages.extend(range(1, 6))
+ pages.append("...")
+ pages.append(total_pages)
+ elif current_page >= total_pages - 3:
+ pages.append(1)
+ pages.append("...")
+ pages.extend(range(total_pages - 4, total_pages + 1))
+ else:
+ pages.append(1)
+ pages.append("...")
+ pages.extend(range(current_page - 1, current_page + 2))
+ pages.append("...")
+ pages.append(total_pages)
+
+ return pages
diff --git a/thatcomputerscientist/urls.py b/thatcomputerscientist/urls.py
index 97d51fcf..5376ee82 100644
--- a/thatcomputerscientist/urls.py
+++ b/thatcomputerscientist/urls.py
@@ -44,6 +44,7 @@ urlpatterns = [
path("journal/", include("apps.journals.urls", namespace="journal")),
path("weblog/", include("apps.blog.urls", namespace="weblog")),
path("pagoda/", include("apps.pagoda.urls", namespace="pagoda")),
+ path("anime/", include("apps.anime.urls", namespace="anime")),
path("services/stream/", include("services.stream.urls", namespace="stream")),
path("services/pamphlet", include("services.pamphlet.urls", namespace="pamphlet")),
path("services/auth/", include("services.users.urls", namespace="auth")),