diff options
| author | Bobby <[email protected]> | 2024-12-21 00:46:45 -0500 |
|---|---|---|
| committer | Bobby <[email protected]> | 2024-12-21 00:46:45 -0500 |
| commit | b453706835fcb5fe960375c7101074f3bb9c1c7a (patch) | |
| tree | 34e3a2e9a6202c032c7703dc9adbb12f530aed70 | |
| parent | 70bd8ebf6e0de202cb3a4f52f39766e69f146053 (diff) | |
| download | thatcomputerscientist-b453706835fcb5fe960375c7101074f3bb9c1c7a.tar.xz thatcomputerscientist-b453706835fcb5fe960375c7101074f3bb9c1c7a.zip | |
anime stream pages
| -rw-r--r-- | apps/anime/__init__.py | 0 | ||||
| -rw-r--r-- | apps/anime/admin.py | 3 | ||||
| -rw-r--r-- | apps/anime/apps.py | 6 | ||||
| -rw-r--r-- | apps/anime/migrations/__init__.py | 0 | ||||
| -rw-r--r-- | apps/anime/models.py | 3 | ||||
| -rw-r--r-- | apps/anime/tests.py | 3 | ||||
| -rw-r--r-- | apps/anime/urls.py | 10 | ||||
| -rw-r--r-- | apps/anime/views.py | 210 | ||||
| -rw-r--r-- | apps/journals/urls.py | 1 | ||||
| -rw-r--r-- | apps/journals/views.py | 15 | ||||
| -rw-r--r-- | static/css/anime/anime.css | 378 | ||||
| -rw-r--r-- | static/images/anime/title_background.png | bin | 0 -> 2313653 bytes | |||
| -rw-r--r-- | static/js/shared/animeList.js | 45 | ||||
| -rw-r--r-- | templates/en/anime/anime.html | 12 | ||||
| -rw-r--r-- | templates/en/anime/home.html | 46 | ||||
| -rw-r--r-- | templates/en/anime/search.html | 78 | ||||
| -rw-r--r-- | templates/en/journals/single.html | 41 | ||||
| -rw-r--r-- | templates/partials/_anime_list.html | 62 | ||||
| -rw-r--r-- | templates/shared/left_sidebar.html | 4 | ||||
| -rw-r--r-- | thatcomputerscientist/settings.py | 2 | ||||
| -rw-r--r-- | thatcomputerscientist/templatetags/color_to_rgb.py | 21 | ||||
| -rw-r--r-- | thatcomputerscientist/templatetags/pagination.py | 43 | ||||
| -rw-r--r-- | thatcomputerscientist/urls.py | 1 |
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 Binary files differnew file mode 100644 index 00000000..da494a07 --- /dev/null +++ b/static/images/anime/title_background.png 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"><</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">></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")), |
