From 7d13b7d4f71ff39907748bcab8f914b83ebb3172 Mon Sep 17 00:00:00 2001 From: ShafathZ Date: Sun, 26 Apr 2026 23:46:12 -0400 Subject: [PATCH 1/9] Updated code to work with anime_enriched db and backend search --- backend/mongo/AniZenithMongoClient.py | 6 +- backend/mongo/AnimeDocument.py | 25 ++- backend/search.py | 198 +++++++++------------- frontend/app.py | 2 +- frontend/static/css/main.css | 8 + frontend/static/js/favorites/favorites.js | 24 +-- frontend/static/js/search/search.js | 12 +- frontend/templates/search.html | 2 +- 8 files changed, 133 insertions(+), 144 deletions(-) diff --git a/backend/mongo/AniZenithMongoClient.py b/backend/mongo/AniZenithMongoClient.py index ffee798..97533d2 100644 --- a/backend/mongo/AniZenithMongoClient.py +++ b/backend/mongo/AniZenithMongoClient.py @@ -39,7 +39,7 @@ def anime_collection(self): """ if self._anime_collection is None: # TODO: Move the hardcoded DB name and collection name into a central Config object - self._anime_collection = self.db_client["anizenith"]["anime"] + self._anime_collection = self.db_client["anizenith"]["anime_enriched"] return self._anime_collection @@ -67,12 +67,14 @@ def add_anime(self, anime_document: AnimeDocument) -> None: self.anime_collection.insert_one(anime_document_dict) - def execute_read_query(self, query, limit = None) -> List[Dict]: + def execute_read_query(self, query, skip = None, limit = None) -> List[Dict]: try: if isinstance(query, dict): # Execute a standard find operation (e.g., {"score": {"$gt": 8.0}}) # "find()" is a standard read-only MongoDB command cursor = self.anime_collection.find(query) + if skip is not None: + cursor = cursor.skip(skip) if limit is not None: cursor = cursor.limit(limit) diff --git a/backend/mongo/AnimeDocument.py b/backend/mongo/AnimeDocument.py index 240e2d1..b7749d0 100644 --- a/backend/mongo/AnimeDocument.py +++ b/backend/mongo/AnimeDocument.py @@ -1,9 +1,24 @@ +from datetime import datetime + from pydantic import BaseModel -from typing import List +from typing import List, Dict + # Class to Model a typical Anime Document class AnimeDocument(BaseModel): - name: str - score: float - synopsis: str - genres: List[str] \ No newline at end of file + mal_id: int # ID of anime on MyAnimeList + name: str # English title of anime + alt_titles: Dict[str, str] # { "en": ENGLISH_TITLE, "jp": JAPANESE_TITLE, ...} + score: float # MAL User Mean Score -- Typically 6-10 + synopsis: str # Short synopsis of shows, does not contain spoilers beyond first episode + genres: List[str] # Genres list not including demographic genre + demographic: str # Primary demographic + age_rating: str # g | pg | pg-13 | r | r+ | rx + cover_image_url: str # link to MAL image (not CDN) + date_aired: datetime # date aired as datetime (YYYY-MM-DD stored, not hr/mins/secs/ms) + status: str # finished_airing | currently_airing | not_aired + episode_count: int # number of episodes in the anime + avg_episode_len_mins: int # average duration per episode in mins + publishing_company: str # publishing company name | Unknown + recommendations: Dict[str, int] # {"TITLE1":#_OF_RECOMMENDATIONS, "TITLE2":#_OF_RECOMMENDATIONS, ...} + node_name: str # Title of anime (node) -- May not be English, but always matches recommendations title \ No newline at end of file diff --git a/backend/search.py b/backend/search.py index 13d54f9..c25baeb 100644 --- a/backend/search.py +++ b/backend/search.py @@ -1,122 +1,74 @@ from datetime import datetime -from typing import List, Optional +from typing import List, Optional, Tuple, Dict, Any from fastapi import Query, APIRouter from pydantic import BaseModel from starlette.requests import Request -search_router = APIRouter() +from backend.mongo.AnimeDocument import AnimeDocument +from backend.utils.model_utils import DB_CLIENT -class Anime(BaseModel): - id: int - cover_image_url: str - title: str - genres: List[str] - short_description: str - score: float - date_added: int +search_router = APIRouter() class SearchResponse(BaseModel): total_count: int - shows: List[Anime] - -def date_to_millis(date_str: str) -> int: - dt = datetime.strptime(date_str, "%b %d, %Y") - return int(dt.timestamp() * 1000) - -# TODO: Remove this and hook up to real database -MOCK_ANIME_LIST = [ - Anime( - id=1, - cover_image_url="https://cdn.myanimelist.net/images/anime/1223/96541.jpg", - title="Fullmetal Alchemist: Brotherhood", - genres=["Action", "Adventure", "Drama", "Fantasy"], - short_description="Two brothers search for the Philosopher's Stone to restore their bodies after a failed alchemy experiment.", - date_added=date_to_millis("Apr 5, 2009"), - score=9.09 - ), - Anime( - id=2, - cover_image_url="https://cdn.myanimelist.net/images/anime/1935/127974.jpg", - title="Steins;Gate", - genres=["Sci-Fi", "Thriller", "Drama"], - short_description="A group of friends accidentally invent a method of sending messages to the past, altering the present.", - date_added=date_to_millis("Apr 6, 2011"), - score=9.07 - ), - Anime( - id=3, - cover_image_url="https://cdn.myanimelist.net/images/anime/1337/99013.jpg", - title="Hunter x Hunter (2011)", - genres=["Action", "Adventure", "Fantasy"], - short_description="Gon Freecss aspires to become a Hunter to find his father, meeting friends and facing deadly challenges.", - date_added=date_to_millis("Oct 2, 2011"), - score=9.03 - ), - Anime( - id=4, - cover_image_url="https://cdn.myanimelist.net/images/anime/10/73274.jpg", - title="Gintama", - genres=["Action", "Comedy", "Sci-Fi"], - short_description="In an alternate Edo period invaded by aliens, a samurai freelancer takes odd jobs to make ends meet.", - date_added=date_to_millis("Apr 4, 2006"), - score=8.94 - ), - Anime( - id=5, - cover_image_url="https://cdn.myanimelist.net/images/anime/1000/110531.jpg", - title="Attack on Titan Final Season", - genres=["Action", "Drama", "Fantasy"], - short_description="The epic conclusion of humanity's battle against the Titans and the truth behind their existence.", - date_added=date_to_millis("Dec 7, 2020"), - score=8.79 - ), - Anime( - id=6, - cover_image_url="https://cdn.myanimelist.net/images/anime/1295/106551.jpg", - title="Kaguya-sama: Love is War", - genres=["Comedy", "Romance", "School"], - short_description="Two geniuses at a prestigious academy engage in psychological warfare to make the other confess love first.", - date_added=date_to_millis("Apr 11, 2020"), - score=8.41 - ), - Anime( - id=7, - cover_image_url="https://cdn.myanimelist.net/images/anime/1500/103005.jpg", - title="Vinland Saga", - genres=["Action", "Adventure", "Drama", "Historical"], - short_description="A young Viking seeks revenge against his father's killer while navigating a world of war and slavery.", - date_added=date_to_millis("Jul 7, 2019"), - score=8.75 - ), - Anime( - id=8, - cover_image_url="https://cdn.myanimelist.net/images/anime/6/86733.jpg", - title="Made in Abyss", - genres=["Adventure", "Drama", "Fantasy", "Mystery"], - short_description="An orphan girl and a robot boy descend into a mysterious, perilous chasm to find her mother.", - date_added=date_to_millis("Jul 7, 2017"), - score=8.65 - ), - Anime( - id=9, - cover_image_url="https://cdn.myanimelist.net/images/anime/5/87048.jpg", - title="Your Name.", - genres=["Drama", "Romance", "Supernatural"], - short_description="Two teenagers swap bodies across time and space, leading to a race against fate.", - date_added=date_to_millis("Aug 26, 2016"), - score=8.84 - ), - Anime( - id=10, - cover_image_url="https://cdn.myanimelist.net/images/anime/6/79597.jpg", - title="Spirited Away", - genres=["Adventure", "Fantasy", "Supernatural"], - short_description="A young girl becomes trapped in a spirit world and must work in a bathhouse to free herself and her parents.", - date_added=date_to_millis("Jul 20, 2001"), - score=8.77 - ), -] + shows: List[AnimeDocument] + +def get_mongo_query( + q: Optional[str] = None, + genre: Optional[List[str]] = None, + year_min: Optional[int] = None, + year_max: Optional[int] = None, + score_min: Optional[float] = None, + score_max: Optional[float] = None, + status: Optional[str] = None, + idx_from: int = 0, + idx_to: int = 19, +) -> Tuple[Dict[str, Any], int, int]: + query: Dict[str, Any] = {} + + # Text search + if q: + regex = {"$regex": q, "$options": "i"} + query["$or"] = [ + {"title": regex}, + {"synopsis": regex}, + ] + + # Genre filter + if genre: + query["genres"] = {"$in": genre} + + # Year range + if year_min is not None or year_max is not None: + year_filter: Dict[str, Any] = {} + if year_min is not None: + year_filter["$gte"] = datetime(year_min, 1, 1) + if year_max is not None: + year_filter["$lt"] = datetime(year_max + 1, 1, 1) + + if year_filter: + query["date_aired"] = year_filter + + # Score range + if score_min is not None or score_max is not None: + score_filter: Dict[str, Any] = {} + if score_min is not None: + score_filter["$gte"] = score_min + if score_max is not None: + score_filter["$lte"] = score_max + if score_filter: + query["score"] = score_filter + + # Exact status match + if status: + query["status"] = status + + # Convert idx_from / idx_to to skip + limit + skip = idx_from + limit = max(0, idx_to - idx_from + 1) + + return query, skip, limit @search_router.get("/anizenith/search") async def search( @@ -133,19 +85,29 @@ async def search( ) -> SearchResponse: """ Search endpoint that returns paginated results. - TODO: Integrate with real backend DB queries """ # Calculate how many items to return in this page - total_results = len(MOCK_ANIME_LIST) - start = idx_from - end = min(idx_to + 1, total_results) + filter_query, skip, limit = get_mongo_query( + q=q, + genre=genre, + year_min=year_min, + year_max=year_max, + score_min=score_min, + score_max=score_max, + status=status, + idx_from=idx_from, + idx_to=idx_to, + ) + + # TODO: Remove count documents as it is expensive + total_count = DB_CLIENT.anime_collection.count_documents(filter_query) + + # TODO: Convert skip and limit to cursor-based search with $gt + docs = DB_CLIENT.execute_read_query(query=filter_query, skip=skip, limit=limit) + shows = [AnimeDocument(**doc) for doc in docs] - shows = [] - for i in range(start, end): - show = MOCK_ANIME_LIST[i].model_copy() - shows.append(show) return SearchResponse( - total_count=total_results, + total_count=total_count, shows=shows ) \ No newline at end of file diff --git a/frontend/app.py b/frontend/app.py index b533ba7..c8cad7b 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -77,7 +77,7 @@ async def add_security_headers(request: Request, call_next): f"default-src 'self'; " f"script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; " f"style-src 'self' https://cdnjs.cloudflare.com; " - f"img-src 'self' https://cdn.myanimelist.net data:; " + f"img-src 'self' https://cdn.myanimelist.net https://myanimelist.net data:; " f"font-src 'self' https://cdnjs.cloudflare.com; " f"frame-ancestors 'none';" ) diff --git a/frontend/static/css/main.css b/frontend/static/css/main.css index 4ee2676..5b27eca 100644 --- a/frontend/static/css/main.css +++ b/frontend/static/css/main.css @@ -25,6 +25,14 @@ body { -ms-user-select: none; } +.truncate { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + + overflow: hidden; +} + /* ===== TOP BAR ===== */ .top-bar { position: fixed; diff --git a/frontend/static/js/favorites/favorites.js b/frontend/static/js/favorites/favorites.js index 599c8eb..be9661b 100644 --- a/frontend/static/js/favorites/favorites.js +++ b/frontend/static/js/favorites/favorites.js @@ -25,7 +25,7 @@ const cardTemplate = $('anime-card-template'); async function fetchFavorites() { try { // TODO: Remove this and rely solely on browser loading, this is purely for testing - const params = new URLSearchParams({ idx_from: '0', idx_to: '999' }); + const params = new URLSearchParams({ idx_from: '0', idx_to: '25' }); const response = await fetch(`${FAVORITES_RETRIEVE_POINT}?${params}`); if (!response.ok) { @@ -60,7 +60,7 @@ function saveFavorites(favs) { async function removeFavorite(animeId) { const index = favorites.findIndex(a => a.id === animeId); if (index !== -1) { - const animeTitle = favorites[index].title; + const animeTitle = favorites[index].name; favorites.splice(index, 1); saveFavorites(favorites); @@ -73,8 +73,8 @@ async function removeFavorite(animeId) { // Lambda expressions for comparing anime favorites stored on browser const sortFunctions = { - titleAsc: (a, b) => a.title.localeCompare(b.title), - titleDesc: (a, b) => b.title.localeCompare(a.title), + titleAsc: (a, b) => a.name.localeCompare(b.name), + titleDesc: (a, b) => b.name.localeCompare(a.name), score: (a, b) => (b.score || 0) - (a.score || 0), dateAdded: (a, b) => (b.date_added || 0) - (a.date_added || 0) }; @@ -86,7 +86,7 @@ function applyFilters() { // Filter by search term if (searchTerm.trim()) { const term = searchTerm.toLowerCase(); - result = result.filter(anime => anime.title.toLowerCase().includes(term)); + result = result.filter(anime => anime.name.toLowerCase().includes(term)); } // Sort using the appropriate function, falling back to dateAdded @@ -106,19 +106,19 @@ function createAnimeCard(anime) { const ratingSpan = clone.querySelector('.rating-value'); // Populate the card with anime data - card.dataset.animeId = anime.id; + card.dataset.animeId = anime.mal_id; img.src = anime.cover_image_url; - img.alt = anime.title; - heartBtn.dataset.id = anime.id; - titleEl.textContent = anime.title; - titleEl.title = anime.title; + img.alt = anime.name; + heartBtn.dataset.id = anime.mal_id; + titleEl.textContent = anime.name; + titleEl.title = anime.name; ratingSpan.textContent = anime.score?.toFixed(1) ?? 'N/A'; // Hold to remove feature: A long press (800ms) triggers removal, a short press does nothing. const handlePointerDown = (e) => { e.preventDefault(); // prevent selecting the text or area behind heartBtn.classList.add('holding'); - heartBtn.holdTimer = setTimeout(() => removeFavorite(anime.id), 800); + heartBtn.holdTimer = setTimeout(() => removeFavorite(anime.mal_id), 800); }; const handlePointerUp = () => { @@ -138,7 +138,7 @@ function createAnimeCard(anime) { // Clicking anywhere on the card (except the heart) navigates to the anime's page card.addEventListener('click', (e) => { if (!e.target.closest('.favorite-heart')) { - window.location.href = `/anime/${anime.id}`; + window.location.href = `/anime/${anime.mal_id}`; } }); diff --git a/frontend/static/js/search/search.js b/frontend/static/js/search/search.js index b5be633..a5e2ecc 100644 --- a/frontend/static/js/search/search.js +++ b/frontend/static/js/search/search.js @@ -167,9 +167,11 @@ async function performSearch() { try { // TODO: Modify fetch url to include pagination params - const res = await fetch(url); + const timeout = 60.0 + const res = await fetch(url, { "X-Request-Timeout": timeout.toString() }); if (!res.ok) postErrorMessage(res.status, "Backend Search Error", API_SEARCH_URL); const data = await res.json(); + console.log(data); totalCount = data.total_count || 0; totalPages = Math.ceil(totalCount / ITEMS_PER_PAGE) || 1; renderResults(data); @@ -194,12 +196,12 @@ function renderShowRow(show) { // Cover image of anime const img = row.querySelector('img'); img.src = show.cover_image_url || ''; - img.alt = escapeHtml(show.title); + img.alt = escapeHtml(show.name); // Anime title const titleEl = row.querySelector('.col-title'); - titleEl.textContent = show.title; - titleEl.title = show.title; + titleEl.textContent = show.name; + titleEl.title = show.name; // Anime genre const genres = Array.isArray(show.genres) ? show.genres.join(', ') : show.genres || ''; @@ -209,7 +211,7 @@ function renderShowRow(show) { // Anime short description const descEl = row.querySelector('.col-desc'); - descEl.textContent = show.short_description || ''; + descEl.textContent = show.synopsis || ''; // Adds row where clicking opens the anime's page const rowElement = row.querySelector('.result-row') diff --git a/frontend/templates/search.html b/frontend/templates/search.html index e1ad90e..123b1cf 100644 --- a/frontend/templates/search.html +++ b/frontend/templates/search.html @@ -108,7 +108,7 @@

Status

-
+
From 0b3b8a8c25003f713a5aa871adda81ec2e6d2f64 Mon Sep 17 00:00:00 2001 From: ShafathZ Date: Thu, 30 Apr 2026 14:29:19 -0400 Subject: [PATCH 2/9] Added search query caching --- backend/search.py | 16 ++++++++++--- pyproject.toml | 1 + uv.lock | 61 ++++++++++++++++++++++++++++------------------- 3 files changed, 50 insertions(+), 28 deletions(-) diff --git a/backend/search.py b/backend/search.py index c25baeb..15a1cff 100644 --- a/backend/search.py +++ b/backend/search.py @@ -1,5 +1,7 @@ from datetime import datetime from typing import List, Optional, Tuple, Dict, Any +from cachetools import TTLCache +from bson import json_util from fastapi import Query, APIRouter from pydantic import BaseModel @@ -10,6 +12,8 @@ search_router = APIRouter() +count_cache = TTLCache(maxsize=1000, ttl=120) + class SearchResponse(BaseModel): total_count: int shows: List[AnimeDocument] @@ -99,10 +103,16 @@ async def search( idx_to=idx_to, ) - # TODO: Remove count documents as it is expensive - total_count = DB_CLIENT.anime_collection.count_documents(filter_query) + # Convert filter query into string for hashing + filter_key = json_util.dumps(filter_query, sort_keys=True) + if filter_key in count_cache: + # Retrieve the total count for this query from cache + total_count = count_cache[filter_key] + else: + # Cache expensive DB operation if not in cache + total_count = DB_CLIENT.anime_collection.count_documents(filter_query) + count_cache[filter_key] = total_count - # TODO: Convert skip and limit to cursor-based search with $gt docs = DB_CLIENT.execute_read_query(query=filter_query, skip=skip, limit=limit) shows = [AnimeDocument(**doc) for doc in docs] diff --git a/pyproject.toml b/pyproject.toml index d1ee309..768af2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "pandas>=3.0.2", "pymongo>=4.16.0", "sentence-transformers>=5.4.0", + "cachetools>=7.0.6", ] [tool.uv.sources] diff --git a/uv.lock b/uv.lock index e4d4eb5..3f6acca 100644 --- a/uv.lock +++ b/uv.lock @@ -47,6 +47,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] +[[package]] +name = "cachetools" +version = "7.0.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/7b/1755ed2c6bfabd1d98b37ae73152f8dcf94aa40fee119d163c19ed484704/cachetools-7.0.6.tar.gz", hash = "sha256:e5d524d36d65703a87243a26ff08ad84f73352adbeafb1cde81e207b456aaf24", size = 37526, upload-time = "2026-04-20T19:02:23.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/c4/cf76242a5da1410917107ff14551764aa405a5fd10cd10cf9a5ca8fa77f4/cachetools-7.0.6-py3-none-any.whl", hash = "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", size = 13976, upload-time = "2026-04-20T19:02:21.187Z" }, +] + [[package]] name = "certifi" version = "2022.12.7" @@ -82,6 +91,7 @@ name = "cs553-casestudy1" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "cachetools" }, { name = "fastapi", extra = ["standard"] }, { name = "huggingface-hub" }, { name = "itsdangerous" }, @@ -97,6 +107,7 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "cachetools", specifier = ">=7.0.6" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.128.0" }, { name = "huggingface-hub", specifier = ">=1.3.5" }, { name = "itsdangerous", specifier = ">=2.2.0" }, @@ -1409,11 +1420,11 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43b35116802c85fb88d99f4a396b8bd4472bfca1dd82e69499e5a4f9b8b4e252" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:442ec9dc78592564fdad69cf0beaa9da2f82ab810ccb4f13903869a90bf3f15d" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cc3a195701bba2239c313ee311487f80f8aaebe9e89b9073dddbcf2f93b5a0ba" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:072a0d6e4865e8b0dc0dbfe6ebed68fae235124222835ef03e5814d414d8c012" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:23ec7789017da9d95b6d543d790814785e6f30905c5443efa8257d1490d73f79" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43b35116802c85fb88d99f4a396b8bd4472bfca1dd82e69499e5a4f9b8b4e252", upload-time = "2026-03-23T15:16:58Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:442ec9dc78592564fdad69cf0beaa9da2f82ab810ccb4f13903869a90bf3f15d", upload-time = "2026-03-23T15:17:02Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cc3a195701bba2239c313ee311487f80f8aaebe9e89b9073dddbcf2f93b5a0ba", upload-time = "2026-03-23T15:17:06Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:072a0d6e4865e8b0dc0dbfe6ebed68fae235124222835ef03e5814d414d8c012", upload-time = "2026-03-23T15:17:10Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:23ec7789017da9d95b6d543d790814785e6f30905c5443efa8257d1490d73f79", upload-time = "2026-03-23T15:17:14Z" }, ] [[package]] @@ -1441,26 +1452,26 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform != 'darwin'" }, ] wheels = [ - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-linux_s390x.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-win_amd64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-linux_s390x.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-win_amd64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-linux_s390x.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-win_amd64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-linux_s390x.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-win_amd64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-linux_s390x.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-win_amd64.whl" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:2db3ae5404e32cb42b5fcbd94f13607761eaec0cf1687fde95095289d1e26cfb", upload-time = "2026-04-28T00:06:06Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:70ecb2659af6373b7c5336e692e665605b0201ea21ff51aaea47e1d75ea6b5aa", upload-time = "2026-04-28T00:06:14Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f82e2ae20c1545bb03997d1cc3143d94e14b800038669ee1aca45808a9acc338", upload-time = "2026-04-28T00:06:24Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:1abeaa46fa7532ed35ed79146f4de5d7a9d4b30462c98052ea4ddfe781ea3eca", upload-time = "2026-04-28T00:06:34Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:d1eff25ccc454faf21c9666c81bfab8e405e87c12d300708d4559620bc191a36", upload-time = "2026-04-28T00:06:42Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:48b3e21a311445acdd0b27f13830e21d93adef70d4721e051e9f059baeb9b8f9", upload-time = "2026-04-28T00:06:51Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:45025d7752dbc6b4c784c03afaee9c5f19730ce084b2e43fc9a2fe1677d9ff86", upload-time = "2026-04-28T00:07:02Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:ed70d4a4fc9f8b826c02fa1a9800a83820fb2fa6ae607680b53390f9ef394d85", upload-time = "2026-04-28T00:07:12Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-linux_s390x.whl", hash = "sha256:65d427a196ab0abe359b93c5bffedd76ded02df2b1b1d2d9f11a2609b69f426a", upload-time = "2026-04-28T00:07:19Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8f13dc7075ae04ca5f876a9f40b4e47522a04c23e30824b4409f42a3f3e57aa4", upload-time = "2026-04-28T00:07:27Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:8713bb8679376ea0ec25742100b6cfb8447e0904c48bddefb9eb0ac1abbfa60a", upload-time = "2026-04-28T00:07:37Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:62ec1f1694c185f601eab74eb7fc0e8e10c64c06ae82f13c3592774c231c4877", upload-time = "2026-04-28T00:07:47Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-linux_s390x.whl", hash = "sha256:c9a14c367f470623b978e273a4e1915995b4ba7a0ae999178b06c273eea3536f", upload-time = "2026-04-28T00:07:54Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:71676f6a9a84bbd385e010198b51fa1c2324fb8f3c512a32d2c81af65f68f4c9", upload-time = "2026-04-28T00:08:02Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:f8481ea9088e4e5b81178a75aabdbb658bde8639bc1a15fd5d8f930abc966735", upload-time = "2026-04-28T00:08:11Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-win_amd64.whl", hash = "sha256:7575af4c9f7f7500ed62b1dafeb069aa0ba35b368a5f09793b3976b3d50f4fe4", upload-time = "2026-04-28T00:08:20Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-linux_s390x.whl", hash = "sha256:825f1596878280a3a4c861441674888bc2d792e4ab7b045cb35feeab3f4f5dd7", upload-time = "2026-04-28T00:08:27Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c8a0bdfb2fd915b6c2cd27c856f63f729c366a4917772eba6b2b02aa3bce70d5", upload-time = "2026-04-28T00:08:36Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:768f22924a25cad2adeb9c6cbac5159e71067c8d4019b1511960d7435a5ca652", upload-time = "2026-04-28T00:08:47Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-win_amd64.whl", hash = "sha256:6db45e7b2526d996fbf47c3d08737807a60a4e17996a6d91a97027fe260832c8", upload-time = "2026-04-28T00:08:57Z" }, ] [[package]] From baf016ea11b1864e42a0c330a38023610b5c0542 Mon Sep 17 00:00:00 2001 From: ShafathZ Date: Thu, 30 Apr 2026 14:34:13 -0400 Subject: [PATCH 3/9] Moved cache constants to config --- backend/configs/app_config.yml | 4 ++++ backend/configs/backend_config.py | 3 +++ backend/search.py | 4 +++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/configs/app_config.yml b/backend/configs/app_config.yml index 501d638..84bd4d1 100644 --- a/backend/configs/app_config.yml +++ b/backend/configs/app_config.yml @@ -4,4 +4,8 @@ default: max_session_cookie_age: 3600 same_site_protection: "lax" + # Count Caching for Pagination + count_cache_time_seconds: 120 + count_cache_max_size: 1000 + log_level: "info" \ No newline at end of file diff --git a/backend/configs/backend_config.py b/backend/configs/backend_config.py index fb661c2..4201f56 100644 --- a/backend/configs/backend_config.py +++ b/backend/configs/backend_config.py @@ -23,6 +23,9 @@ class BackendAppConfig(Config): max_session_cookie_age: Optional[int] = None same_site_protection: Optional[str] = None + count_cache_time_seconds: Optional[int] = None + count_cache_max_size: Optional[int] = None + HF_TOKEN: str = os.getenv("HF_TOKEN", "") MAL_CLIENT_ID: str = os.getenv("MAL_CLIENT_ID", "") MAL_CLIENT_SECRET: str = os.getenv("MAL_CLIENT_SECRET", "") diff --git a/backend/search.py b/backend/search.py index 15a1cff..f1bcc05 100644 --- a/backend/search.py +++ b/backend/search.py @@ -10,9 +10,11 @@ from backend.mongo.AnimeDocument import AnimeDocument from backend.utils.model_utils import DB_CLIENT +from backend.configs import backend_app_config + search_router = APIRouter() -count_cache = TTLCache(maxsize=1000, ttl=120) +count_cache = TTLCache(maxsize=backend_app_config.count_cache_max_size, ttl=backend_app_config.count_cache_time_seconds) class SearchResponse(BaseModel): total_count: int From 4ffa6ced4a1444d05fdf4bf5db147d84e00e482d Mon Sep 17 00:00:00 2001 From: ShafathZ Date: Fri, 1 May 2026 21:48:31 -0400 Subject: [PATCH 4/9] Merged from main --- uv.lock | 51 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/uv.lock b/uv.lock index 558824c..7d98af1 100644 --- a/uv.lock +++ b/uv.lock @@ -47,6 +47,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] +[[package]] +name = "cachetools" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/1d/7cbe8f84352789be3dcbb1239f425984f353efa253deeeff38af600870b3/cachetools-7.1.0.tar.gz", hash = "sha256:ea5406e92956f9006b121f8032177c6b02cc5f9a488d1e53b2e4d9cb5aae15c6", size = 40085, upload-time = "2026-05-01T21:20:04.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/42/e09974bc74ea2791f985dff3d7f06065d11abcc31da6c46fc5f83046516e/cachetools-7.1.0-py3-none-any.whl", hash = "sha256:05afd1d309309e7c8971db462b4cf516d93fa8c9aea1b906e26e61b4399d0d82", size = 16748, upload-time = "2026-05-01T21:20:02.879Z" }, +] + [[package]] name = "certifi" version = "2026.4.22" @@ -155,6 +164,7 @@ name = "cs553-casestudy1" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "cachetools" }, { name = "fastapi", extra = ["standard"] }, { name = "huggingface-hub" }, { name = "itsdangerous" }, @@ -171,6 +181,7 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "cachetools", specifier = ">=7.0.6" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.128.0" }, { name = "huggingface-hub", specifier = ">=1.3.5" }, { name = "itsdangerous", specifier = ">=2.2.0" }, @@ -1531,26 +1542,26 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform != 'darwin'" }, ] wheels = [ - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-linux_s390x.whl", upload-time = "2026-03-23T14:59:01Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", upload-time = "2026-03-23T14:59:02Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", upload-time = "2026-03-23T14:59:03Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-win_amd64.whl", upload-time = "2026-03-23T14:59:04Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-linux_s390x.whl", upload-time = "2026-03-23T14:59:04Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", upload-time = "2026-03-23T14:59:04Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", upload-time = "2026-03-23T14:59:05Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-win_amd64.whl", upload-time = "2026-03-23T14:59:06Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-linux_s390x.whl", upload-time = "2026-03-23T14:59:07Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", upload-time = "2026-03-23T14:59:07Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", upload-time = "2026-03-23T14:59:07Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-win_amd64.whl", upload-time = "2026-03-23T14:59:09Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-linux_s390x.whl", upload-time = "2026-03-23T14:59:09Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", upload-time = "2026-03-23T14:59:10Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", upload-time = "2026-03-23T14:59:11Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-win_amd64.whl", upload-time = "2026-03-23T14:59:12Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-linux_s390x.whl", upload-time = "2026-03-23T14:59:12Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", upload-time = "2026-03-23T14:59:12Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", upload-time = "2026-03-23T14:59:13Z" }, - { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-win_amd64.whl", upload-time = "2026-03-23T14:59:15Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:2db3ae5404e32cb42b5fcbd94f13607761eaec0cf1687fde95095289d1e26cfb", upload-time = "2026-04-28T00:06:06Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:70ecb2659af6373b7c5336e692e665605b0201ea21ff51aaea47e1d75ea6b5aa", upload-time = "2026-04-28T00:06:14Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f82e2ae20c1545bb03997d1cc3143d94e14b800038669ee1aca45808a9acc338", upload-time = "2026-04-28T00:06:24Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:1abeaa46fa7532ed35ed79146f4de5d7a9d4b30462c98052ea4ddfe781ea3eca", upload-time = "2026-04-28T00:06:34Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:d1eff25ccc454faf21c9666c81bfab8e405e87c12d300708d4559620bc191a36", upload-time = "2026-04-28T00:06:42Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:48b3e21a311445acdd0b27f13830e21d93adef70d4721e051e9f059baeb9b8f9", upload-time = "2026-04-28T00:06:51Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:45025d7752dbc6b4c784c03afaee9c5f19730ce084b2e43fc9a2fe1677d9ff86", upload-time = "2026-04-28T00:07:02Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:ed70d4a4fc9f8b826c02fa1a9800a83820fb2fa6ae607680b53390f9ef394d85", upload-time = "2026-04-28T00:07:12Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-linux_s390x.whl", hash = "sha256:65d427a196ab0abe359b93c5bffedd76ded02df2b1b1d2d9f11a2609b69f426a", upload-time = "2026-04-28T00:07:19Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8f13dc7075ae04ca5f876a9f40b4e47522a04c23e30824b4409f42a3f3e57aa4", upload-time = "2026-04-28T00:07:27Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:8713bb8679376ea0ec25742100b6cfb8447e0904c48bddefb9eb0ac1abbfa60a", upload-time = "2026-04-28T00:07:37Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:62ec1f1694c185f601eab74eb7fc0e8e10c64c06ae82f13c3592774c231c4877", upload-time = "2026-04-28T00:07:47Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-linux_s390x.whl", hash = "sha256:c9a14c367f470623b978e273a4e1915995b4ba7a0ae999178b06c273eea3536f", upload-time = "2026-04-28T00:07:54Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:71676f6a9a84bbd385e010198b51fa1c2324fb8f3c512a32d2c81af65f68f4c9", upload-time = "2026-04-28T00:08:02Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:f8481ea9088e4e5b81178a75aabdbb658bde8639bc1a15fd5d8f930abc966735", upload-time = "2026-04-28T00:08:11Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-win_amd64.whl", hash = "sha256:7575af4c9f7f7500ed62b1dafeb069aa0ba35b368a5f09793b3976b3d50f4fe4", upload-time = "2026-04-28T00:08:20Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-linux_s390x.whl", hash = "sha256:825f1596878280a3a4c861441674888bc2d792e4ab7b045cb35feeab3f4f5dd7", upload-time = "2026-04-28T00:08:27Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c8a0bdfb2fd915b6c2cd27c856f63f729c366a4917772eba6b2b02aa3bce70d5", upload-time = "2026-04-28T00:08:36Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:768f22924a25cad2adeb9c6cbac5159e71067c8d4019b1511960d7435a5ca652", upload-time = "2026-04-28T00:08:47Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-win_amd64.whl", hash = "sha256:6db45e7b2526d996fbf47c3d08737807a60a4e17996a6d91a97027fe260832c8", upload-time = "2026-04-28T00:08:57Z" }, ] [[package]] From 3ec5f3d198df600a01960b7bde9e7bde51654627 Mon Sep 17 00:00:00 2001 From: ShafathZ Date: Fri, 1 May 2026 22:20:51 -0400 Subject: [PATCH 5/9] Fixed issues with proxy forwarding variable length queries --- backend/search.py | 5 ++++- frontend/app.py | 2 +- frontend/static/js/search/search.js | 1 + frontend/templates/search.html | 5 ++--- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/backend/search.py b/backend/search.py index f1bcc05..03b8278 100644 --- a/backend/search.py +++ b/backend/search.py @@ -43,7 +43,8 @@ def get_mongo_query( # Genre filter if genre: - query["genres"] = {"$in": genre} + genre = [g.capitalize() for g in genre] # Consistent with capitalized genres in db + query["genres"] = {"$all": genre} # Check that all genres requested match # Year range if year_min is not None or year_max is not None: @@ -105,6 +106,8 @@ async def search( idx_to=idx_to, ) + print(filter_query) + # Convert filter query into string for hashing filter_key = json_util.dumps(filter_query, sort_keys=True) if filter_key in count_cache: diff --git a/frontend/app.py b/frontend/app.py index c8cad7b..6656034 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -136,7 +136,7 @@ async def proxy(path: str, request: Request): url=backend_url, content=body, headers=dict(request.headers), - params=request.query_params, + params=list(request.query_params.multi_items()), # Line supports multi-query parameters (e.g. lists) ) except (httpx.ConnectError, httpx.TimeoutException): return JSONResponse({"error": "Backend server has timed out. Please try again later."}, status_code=504) diff --git a/frontend/static/js/search/search.js b/frontend/static/js/search/search.js index be5c3b9..6e0071b 100644 --- a/frontend/static/js/search/search.js +++ b/frontend/static/js/search/search.js @@ -164,6 +164,7 @@ async function performSearch() { const params = buildQueryString(getCurrentFilters()); const url = `${API_SEARCH_URL}?${params}`; history.replaceState(null, '', `?${params}`); + console.log(url); try { // TODO: Modify fetch url to include pagination params diff --git a/frontend/templates/search.html b/frontend/templates/search.html index 123b1cf..5c1fe52 100644 --- a/frontend/templates/search.html +++ b/frontend/templates/search.html @@ -84,9 +84,8 @@

Score

Status

From 6fdbb076d88e5b3be4e65ca773caa9f2b770a252 Mon Sep 17 00:00:00 2001 From: ShafathZ Date: Tue, 5 May 2026 00:45:35 -0400 Subject: [PATCH 6/9] Updated UI look of search page and anime cards, made Favorites page mirror, moved DB pushing in favorites into separate file. Also, ensured Search page is fully integrated with backend calls --- frontend/static/css/animegrid.css | 288 +++++++++++++++ frontend/static/css/favorites/favorites.css | 345 +++++------------- frontend/static/css/main.css | 42 +++ frontend/static/css/search/search.css | 159 +------- frontend/static/js/animecard.js | 128 +++++++ frontend/static/js/chatbot/chat_utils.js | 2 +- frontend/static/js/favorites/favorites.js | 308 +++++----------- .../chatHistoryDB.js} | 0 frontend/static/js/localDBs/favoritesDB.js | 42 +++ frontend/static/js/search/search.js | 74 +--- .../components/anime-card-template.html | 35 ++ .../components/pagination-template.html | 17 + frontend/templates/favorites.html | 109 +++--- frontend/templates/search.html | 221 +++++------ 14 files changed, 878 insertions(+), 892 deletions(-) create mode 100644 frontend/static/css/animegrid.css create mode 100644 frontend/static/js/animecard.js rename frontend/static/js/{chatbot/chat_history_db.js => localDBs/chatHistoryDB.js} (100%) create mode 100644 frontend/static/js/localDBs/favoritesDB.js create mode 100644 frontend/templates/components/anime-card-template.html create mode 100644 frontend/templates/components/pagination-template.html diff --git a/frontend/static/css/animegrid.css b/frontend/static/css/animegrid.css new file mode 100644 index 0000000..62b8783 --- /dev/null +++ b/frontend/static/css/animegrid.css @@ -0,0 +1,288 @@ +/* ===== Anime Card Grid ===== */ +.results-container { + display: flex; + flex-wrap: wrap; + gap: 13px; + justify-content: flex-start; + align-items: stretch; + padding: 10px 0; +} + +.results-wrapper { + background: var(--bg-glass-darker); + backdrop-filter: blur(18px); + border-radius: 18px; + border: 1px solid var(--border-soft); + padding: 15px; + + height: 100%; + overflow-y: auto; + display: flex; + flex-direction: column; + justify-content: flex-start; + margin-bottom: 10px; +} + +.anime-card { + display: flex; + flex-direction: row; + background: var(--bg-glass); + border-radius: 20px; + overflow: hidden; + border: 1px solid var(--border-soft); + box-shadow: var(--shadow-soft); + transition: transform 0.25s ease, box-shadow 0.3s ease; + cursor: default; + width: auto; + min-width: 200px; + height: 300px; + position: relative; +} + +.anime-card:hover { + transform: translateY(-8px); + box-shadow: 0 20px 30px rgba(0, 0, 0, 0.5), 0 0 0 2px var(--accent-primary); +} + +.card-image-area { + position: relative; + width: 200px; + height: 100%; + border-radius: 20px; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-color: var(--bg-glass-darker); + flex-shrink: 0; +} + +.card-genres { + position: absolute; + top: 12px; + left: 12px; + display: flex; + flex-wrap: wrap; + gap: 6px; + z-index: 2; +} + +.genre-pill { + background: var(--bg-glass-strong); + backdrop-filter: blur(4px); + padding: 4px 10px; + border-radius: 40px; + font-size: 0.7rem; + font-weight: 500; + color: var(--text-main); + border: 1px solid var(--accent-glow); + letter-spacing: 0.3px; + white-space: nowrap; +} + +.card-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 0.8rem; + background: var(--bg-glass-strong); + background: linear-gradient(to top, var(--bg-glass-strong) 0%, transparent 100%); + backdrop-filter: blur(2px); + color: var(--text-main); + border-radius: 0 0 20px 20px; + z-index: 1; + box-sizing: border-box; + text-align: center; +} + +.card-title { + font-weight: 700; + font-size: 0.85rem; + margin: 0 0 4px 0; + white-space: normal; + word-break: break-word; + overflow: visible; + line-height: 1.3; + max-height: 2.6em; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +/* ===== Card Bottom Icons ===== */ +.card-bottom-icons { + position: absolute; + bottom: 8px; + left: 8px; + right: 8px; + display: flex; + justify-content: space-between; + align-items: center; + z-index: 10; + pointer-events: none; +} + +.favorite-heart, +.chat-icon { + width: 32px; + height: 32px; + border-radius: 50%; + background: var(--bg-glass-strong); + backdrop-filter: blur(4px); + border: 1px solid var(--border-soft); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.1rem; + transition: all 0.2s ease; + box-shadow: var(--shadow-soft); + pointer-events: auto; +} + +.favorite-heart { + color: var(--accent-primary); + cursor: pointer; +} + +.favorite-heart:hover, +.chat-icon:hover { + background: var(--accent-secondary); + color: white; + transform: scale(1.1); + border-color: transparent; +} + +.card-score { + font-size: 0.75rem; + font-weight: 500; + color: #ffb83d; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + white-space: nowrap; +} + +.card-score i { + font-size: 0.7rem; + color: #ffb83d; +} + +/* ===== Broken Heart animation ===== */ +.favorite-heart.holding i { + opacity: 0; +} + +.favorite-heart.holding::before, +.favorite-heart.holding::after { + content: "\f004"; + font-family: "Font Awesome 6 Free"; + font-weight: 900; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + color: var(--accent-primary); + font-size: inherit; + pointer-events: none; +} + +.favorite-heart.holding::before { + clip-path: polygon(0% 0%, 50% 0%, 50% 100%, 0% 100%); + animation: leftHalfBreak 1.8s ease-in-out forwards; +} + +.favorite-heart.holding::after { + clip-path: polygon(50% 0%, 100% 0%, 100% 100%, 50% 100%); + animation: rightHalfBreak 1.8s ease-in-out forwards; +} + +@keyframes leftHalfBreak { + 0% { + transform: translate(-50%, -50%) rotate(0deg); + opacity: 1; + } + 100% { + transform: translate(-200%, -120%) rotate(-45deg); + opacity: 0.5; + } +} + +@keyframes rightHalfBreak { + 0% { + transform: translate(-50%, -50%) rotate(0deg); + opacity: 1; + } + 100% { + transform: translate(100%, -120%) rotate(45deg); + opacity: 0.5; + } +} + +/* ===== Expanded Side Section ===== */ +.expand-arrow { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + width: 32px; + height: 32px; + background: var(--bg-glass-strong); + backdrop-filter: blur(4px); + border: 1px solid var(--border-soft); + border-radius: 30px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: var(--text-main); + font-size: 1rem; + transition: all 0.2s ease; + z-index: 3; + box-shadow: var(--shadow-soft); +} + +.expand-arrow:hover { + background: var(--accent-primary); + color: white; + transform: translateY(-50%) scale(1.05); + border-color: transparent; +} + +.card-expand-panel { + width: 0; + overflow: hidden; + transition: width 0.3s ease-out; + background: var(--bg-glass-strong); + border-left: 1px solid transparent; + display: flex; + flex-direction: column; + gap: 12px; + padding: 0; + white-space: normal; + word-break: break-word; + height: 100%; + overflow-y: auto; +} + +.anime-card.expanded .card-expand-panel { + width: 190px; + padding: 16px 12px; + border-left-color: var(--border-soft); +} + +.expand-info p { + margin: 8px 0; + font-size: 0.85rem; + color: var(--text-muted); + line-height: 1.4; +} + +.expand-info strong { + color: var(--text-main); + font-weight: 600; +} + +.expand-description { + font-size: 0.8rem; +} \ No newline at end of file diff --git a/frontend/static/css/favorites/favorites.css b/frontend/static/css/favorites/favorites.css index 8a8ec65..a165401 100644 --- a/frontend/static/css/favorites/favorites.css +++ b/frontend/static/css/favorites/favorites.css @@ -1,307 +1,130 @@ @import url("../pagination.css"); +@import url("../animegrid.css"); -/* ===== Primary Container ===== */ -.favorites-container { - max-width: 900px; - height: 60vh; - margin: 0 auto 1rem; - background: var(--bg-glass-strong); - border-radius: 22px; - padding: 25px; - border: 1px solid var(--border-soft); - box-shadow: var(--shadow-soft); - - display: flex; - flex-direction: column; - overflow-y: auto; -} - -/* ===== Favorites Options ===== */ +/* ===== Favorites Search Options ===== */ .favorites-options { - display: flex; - align-items: center; - gap: 1rem; - flex-wrap: wrap; + flex-shrink: 0; + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + margin-bottom: 1rem; } .sort-wrapper, .search-favorites-wrapper { - display: flex; - align-items: center; - background: var(--bg-glass-darker); - border-radius: 40px; - padding: 0.2rem 0.2rem 0.2rem 1rem; - border: 1px solid var(--border-soft); - backdrop-filter: blur(8px); + display: flex; + align-items: center; + background: var(--bg-glass-darker); + border-radius: 40px; + padding: 0.2rem 0.2rem 0.2rem 1rem; + border: 1px solid var(--border-soft); + backdrop-filter: blur(8px); } .sort-label { - color: var(--text-muted); - margin-right: 8px; + color: var(--text-muted); + margin-right: 8px; } .sort-select { - background: transparent; - border: none; - color: var(--text-main); - font-weight: 500; - padding: 10px 30px 10px 10px; - border-radius: 40px; - cursor: pointer; - outline: none; - appearance: none; + background: transparent; + border: none; + color: var(--text-main); + font-weight: 500; + padding: 10px 30px 10px 10px; + border-radius: 40px; + cursor: pointer; + outline: none; + appearance: none; } .sort-select option { - background: var(--bg-main); - color: var(--text-main); + background: var(--bg-main); + color: var(--text-main); } .search-favorites-wrapper .search-icon { - color: var(--text-muted); - margin-right: 8px; + color: var(--text-muted); + margin-right: 8px; } .search-favorites-input { - background: transparent; - border: none; - color: var(--text-main); - padding: 10px 10px 10px 0; - width: 220px; - outline: none; - font-size: 0.95rem; + background: transparent; + border: none; + color: var(--text-main); + padding: 10px 10px 10px 0; + width: 220px; + outline: none; + font-size: 0.95rem; } .search-favorites-input::placeholder { - color: var(--text-muted); - opacity: 0.7; + color: var(--text-muted); + opacity: 0.7; } .clear-search-btn { - background: transparent; - border: none; - color: var(--text-muted); - cursor: pointer; - padding: 8px 12px; - border-radius: 50%; - transition: background 0.2s; + background: transparent; + border: none; + color: var(--text-muted); + cursor: pointer; + padding: 8px 12px; + border-radius: 50%; + transition: background 0.2s; } .clear-search-btn:hover { - background: var(--bg-glass); - color: var(--text-main); -} - -/* ===== Favorites Grid Section ===== */ -.favorites-bottom { - display: flex; - flex-direction: column; - justify-content: center; - flex: 1; -} - -.favorites-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - gap: 1.8rem; - margin: 3rem 0rem; + background: var(--bg-glass); + color: var(--text-main); } -.anime-card { - position: relative; - border-radius: 20px; - overflow: hidden; - border: 1px solid var(--border-soft); - transition: transform 0.25s ease, box-shadow 0.3s ease; - box-shadow: var(--shadow-soft); - cursor: pointer; - aspect-ratio: 2/3; -} - -.anime-card:hover { - transform: translateY(-8px); - box-shadow: 0 20px 30px rgba(0, 0, 0, 0.5), 0 0 0 2px var(--accent-primary); -} - -.anime-card:hover .card-cover img { - transform: scale(1.05); -} - -.favorite-heart { - position: absolute; - top: 10px; - right: 10px; - width: 36px; - height: 36px; - border-radius: 50%; - background: var(--bg-glass-strong); - backdrop-filter: blur(4px); - border: 1px solid var(--border-soft); - display: flex; - align-items: center; - justify-content: center; - color: var(--accent-primary); - font-size: 1.3rem; - cursor: pointer; - transition: all 0.2s ease; - z-index: 5; - box-shadow: var(--shadow-soft); -} - -.favorite-heart:hover { - background: var(--accent-secondary); - color: white; - transform: scale(1.1); - border-color: transparent; -} - -/* Hide the normal heart icon during hold */ -.favorite-heart.holding i { - opacity: 0; -} - -/* Left half of broken heart */ -.favorite-heart.holding::before { - content: "\f004"; - font-family: "Font Awesome 6 Free"; - font-weight: 900; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - color: var(--accent-primary); - font-size: inherit; - clip-path: polygon(0% 0%, 50% 0%, 50% 100%, 0% 100%); - animation: leftHalfBreak 1.8s ease-in-out forwards; - pointer-events: none; -} - -/* Right half of broken heart */ -.favorite-heart.holding::after { - content: "\f004"; - font-family: "Font Awesome 6 Free"; - font-weight: 900; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - color: var(--accent-primary); - font-size: inherit; - clip-path: polygon(50% 0%, 100% 0%, 100% 100%, 50% 100%); - animation: rightHalfBreak 1.8s ease-in-out forwards; - pointer-events: none; -} - -.favorite-heart.holding::before, -.favorite-heart.holding::after { - animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94); -} - -@keyframes leftHalfBreak { - 0% { - transform: translate(-50%, -50%) rotate(0deg); - opacity: 1; - } - 100% { - transform: translate(-200%, -120%) rotate(-45deg); - opacity: 0.5; - } -} - -@keyframes rightHalfBreak { - 0% { - transform: translate(-50%, -50%) rotate(0deg); - opacity: 1; - } - 100% { - transform: translate(100%, -120%) rotate(45deg); - opacity: 0.5; - } -} - -.card-overlay { - position: absolute; - bottom: 0; - left: 0; - right: 0; - padding: 1.2rem 0.8rem 0.8rem; - background: linear-gradient(to top, var(--bg-glass-strong) 0%, transparent 100%); - backdrop-filter: blur(6px); - color: var(--text-main); - border-radius: 0 0 20px 20px; - overflow: hidden; -} - -.card-title { - font-weight: 700; - font-size: 1rem; - margin-bottom: 4px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.card-meta { - display: flex; - justify-content: space-between; - color: var(--text-muted); - font-size: 0.8rem; -} - -.card-rating { - display: flex; - align-items: center; - gap: 4px; -} - -.card-rating i { - color: #ffb83d; -} - -/* ===== No favorites state ===== */ -.empty-state { - text-align: center; - padding: 4rem 2rem; - background: var(--bg-glass); - border-radius: 40px; - backdrop-filter: blur(12px); - border: 1px solid var(--border-soft); - margin: 1rem 0; +/* ===== No Results State ===== */ +.no-results { + text-align: center; + padding: 4rem 2rem; + background: var(--bg-glass); + border-radius: 40px; + backdrop-filter: blur(12px); + border: 1px solid var(--border-soft); + margin: 1rem 0; + width: 100%; + box-sizing: border-box; } -.empty-illustration { - font-size: 5rem; - color: var(--accent-primary); - opacity: 0.7; - margin-bottom: 1.5rem; +.no-results-illustration { + font-size: 5rem; + color: var(--accent-primary); + opacity: 0.7; + margin-bottom: 1.5rem; } -.empty-state h2 { - font-size: 2rem; - margin-bottom: 0.5rem; - color: var(--text-main); +.no-results h2 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: var(--text-main); } -.empty-state p { - color: var(--text-muted); - margin-bottom: 2rem; +.no-results p { + color: var(--text-muted); + margin-bottom: 2rem; } .browse-btn { - display: inline-flex; - align-items: center; - gap: 10px; - padding: 12px 28px; - background: var(--accent-gradient); - border-radius: 40px; - color: white; - font-weight: 600; - text-decoration: none; - box-shadow: var(--btn-glow); - transition: transform 0.2s, box-shadow 0.2s; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 12px 28px; + background: var(--accent-gradient); + border-radius: 40px; + color: white; + font-weight: 600; + text-decoration: none; + box-shadow: var(--btn-glow); + transition: transform 0.2s, box-shadow 0.2s; } .browse-btn:hover { - transform: translateY(-3px); - box-shadow: 0 0 25px var(--accent-glow); + transform: translateY(-3px); + box-shadow: 0 0 25px var(--accent-glow); } \ No newline at end of file diff --git a/frontend/static/css/main.css b/frontend/static/css/main.css index 5b27eca..780c92e 100644 --- a/frontend/static/css/main.css +++ b/frontend/static/css/main.css @@ -33,6 +33,22 @@ body { overflow: hidden; } +/* ===== Main Container ===== */ +.page-container { + max-width: 900px; + height: 70vh; + margin: 0 auto 1rem; + background: var(--bg-glass-strong); + border-radius: 22px; + padding: 25px; + border: 1px solid var(--border-soft); + box-shadow: var(--shadow-soft); + + display: flex; + flex-direction: column; + overflow: hidden; +} + /* ===== TOP BAR ===== */ .top-bar { position: fixed; @@ -393,4 +409,30 @@ body { margin-left: auto; margin-right: auto; margin-bottom: 1rem; +} + +/* ===== Custom Scrollbar Object ===== */ +.custom-scrollbar { + scrollbar-width: thin; + scrollbar-color: var(--accent-primary) var(--bg-glass-darker); +} + +.custom-scrollbar::-webkit-scrollbar { + width: 6px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: var(--bg-glass-darker); + border-radius: 10px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background: var(--accent-primary); + border-radius: 10px; + opacity: 0.7; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: var(--accent-secondary); + opacity: 1; } \ No newline at end of file diff --git a/frontend/static/css/search/search.css b/frontend/static/css/search/search.css index 3a06792..e9f91c3 100644 --- a/frontend/static/css/search/search.css +++ b/frontend/static/css/search/search.css @@ -1,24 +1,9 @@ @import url("../pagination.css"); - -/* ===== Main Search Container ===== */ -.search-container { - max-width: 900px; - height: 70vh; - margin: 0 auto 1rem; - background: var(--bg-glass-strong); - border-radius: 22px; - padding: 25px; - border: 1px solid var(--border-soft); - box-shadow: var(--shadow-soft); - - display: flex; - flex-direction: column; - overflow: hidden; -} +@import url("../animegrid.css"); /* ===== Search Section ===== */ .search-section { - padding: 0 0 1.2rem 0; + padding: 0; flex-shrink: 0; } @@ -326,141 +311,6 @@ box-shadow: var(--btn-glow); } -/* ===== Results Container ===== */ -.results-wrapper { - background: var(--bg-glass-darker); - backdrop-filter: blur(18px); - border-radius: 18px; - border: 1px solid var(--border-soft); - padding: 15px; - - height: 100%; - - overflow-y: auto; - display: flex; - flex-direction: column; - justify-content: center; - - scrollbar-width: thin; - scrollbar-color: var(--accent-primary) var(--bg-glass-darker); -} - -.results-wrapper:has(.results-table) { - justify-content: flex-start; -} - -.results-wrapper::-webkit-scrollbar { - width: 6px; -} - -.results-wrapper::-webkit-scrollbar-track { - background: var(--bg-glass-darker); - border-radius: 10px; -} - -.results-wrapper::-webkit-scrollbar-thumb { - background: var(--accent-primary); - border-radius: 10px; - opacity: 0.7; -} - -/* ===== Results Table ===== */ -.results-table { - width: 100%; - display: flex; - flex-direction: column; - gap: 10px; - margin: 0; -} - -.results-header { - display: flex; - align-items: center; - padding: 0 10px 8px; - margin-bottom: 2px; -} - -.results-header span { - color: var(--text-main); - font-weight: 500; - font-size: 0.85rem; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.result-row { - display: flex; - align-items: center; - background: var(--bg-glass); - backdrop-filter: blur(4px); - border-radius: 14px; - padding: 12px 10px; - margin-bottom: 10px; - box-shadow: var(--shadow-soft); - border: 1px solid var(--border-soft); - transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s; -} - -.result-row:hover { - transform: translateY(-2px); - box-shadow: 0 8px 20px rgba(0,0,0,0.3), 0 0 15px var(--accent-glow); - border-color: var(--accent-secondary); -} - -.col-cover { - flex: 0 0 80px; -} - -.col-title { - flex: 2; - min-width: 0; - padding: 0 8px; -} - -.col-genres { - flex: 1.5; - min-width: 0; - padding: 0 8px; -} - -.col-desc { - flex: 3; - min-width: 0; - padding: 0 8px; -} - -.cover-art img { - width: 60px; - height: 85px; - object-fit: cover; - border-radius: 8px; - box-shadow: 0 4px 8px rgba(0,0,0,0.4); - display: block; -} - -.title-col { - font-weight: 600; - font-size: 1rem; - color: var(--text-main); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.genres-col { - color: var(--text-muted); - font-size: 0.9rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.desc-col { - color: var(--text-muted); - font-size: 0.9rem; - line-height: 1.4; -} - /* ===== No Results State ===== */ .no-results { text-align: center; @@ -478,4 +328,9 @@ opacity: 0.6; margin-bottom: 1rem; display: block; +} + +.no-results { + margin: 2rem auto; + width: 100%; } \ No newline at end of file diff --git a/frontend/static/js/animecard.js b/frontend/static/js/animecard.js new file mode 100644 index 0000000..4c8c8c8 --- /dev/null +++ b/frontend/static/js/animecard.js @@ -0,0 +1,128 @@ +import { getFavorites, saveFavorites, isAnimeFavorited, addFavorite, removeFavorite } from './localDBs/favoritesDB.js' +import { postErrorMessage } from './error.js'; + +// Template cloning and base setup +export function renderAnimeCard(show) { + const template = document.getElementById('tmpl-anime-card'); + const card = template.content.cloneNode(true).firstElementChild; + card.setAttribute('data-id', show.mal_id); + + // Card image + const imageArea = card.querySelector('.card-image-area'); + imageArea.style.backgroundImage = `url(${show.cover_image_url})`; + + // Genre pills + const genresContainer = card.querySelector('.card-genres'); + show.genres.slice(0, 3).forEach(genre => { + const pill = document.createElement('span'); + pill.className = 'genre-pill'; + pill.textContent = genre; + genresContainer.appendChild(pill); + }); + + // Title and score + const titleEl = card.querySelector('.card-title'); + titleEl.textContent = show.name; + titleEl.title = show.name; + const scoreSpan = card.querySelector('.card-score span'); + scoreSpan.textContent = parseFloat(show.score).toFixed(1); + + // Expand panel (studio, aired, synopsis) + const studioSpan = card.querySelector('.studio'); + studioSpan.textContent = show.publishing_company; + const airedSpan = card.querySelector('.aired'); + const date = new Date(show.date_aired); + airedSpan.textContent = date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); + const synopsisSpan = card.querySelector('.synopsis'); + synopsisSpan.textContent = show.synopsis; + + // Favorite heart (add/remove with long press) + const heartBtn = card.querySelector('.favorite-heart'); + heartBtn.setAttribute('data-id', show.mal_id); + const currentlyFavorited = isAnimeFavorited(show.mal_id); + heartBtn.innerHTML = currentlyFavorited ? '' : ''; + heartBtn.setAttribute('data-favorited', currentlyFavorited ? 'true' : 'false'); + + let pressTimer = null, isLongPress = false; + const addFavoriteBtnFunc = async () => { + if (heartBtn.getAttribute('data-favorited') === 'true') return; + heartBtn.innerHTML = ''; + heartBtn.setAttribute('data-favorited', 'true'); + addFavorite(show); + postErrorMessage(101, "Added Favorite", `Added "${show.name}" to favorites`); + }; + const removeFavoriteBtnFunc = async () => { + if (heartBtn.getAttribute('data-favorited') === 'false') return; + heartBtn.addEventListener('animationend', () => { + heartBtn.classList.remove('holding'); + heartBtn.innerHTML = ''; + heartBtn.setAttribute('data-favorited', 'false'); + removeFavorite(show.mal_id); + postErrorMessage(102, "Removed Favorite", `Removed "${show.name}" from favorites`); + }, { once: true }); + }; + const cancelLongPress = () => { + if (pressTimer) clearTimeout(pressTimer); + heartBtn.classList.remove('holding'); + isLongPress = false; + }; + const handlePointerDown = (e) => { + e.preventDefault(); + isLongPress = false; + heartBtn.classList.add('holding'); + pressTimer = setTimeout(() => { isLongPress = true; removeFavoriteBtnFunc(); }, 800); + }; + const handlePointerUp = () => { + if (pressTimer) clearTimeout(pressTimer); + if (!isLongPress) heartBtn.classList.remove('holding'); + isLongPress = false; + }; + const handleClickInactive = (e) => { e.stopPropagation(); e.preventDefault(); addFavoriteBtnFunc(); }; + function updateHeartListeners() { + const isFavorited = heartBtn.getAttribute('data-favorited') === 'true'; + heartBtn.removeEventListener('click', handleClickInactive); + heartBtn.removeEventListener('pointerdown', handlePointerDown); + heartBtn.removeEventListener('pointerup', handlePointerUp); + heartBtn.removeEventListener('pointercancel', cancelLongPress); + heartBtn.removeEventListener('pointerleave', cancelLongPress); + if (!isFavorited) { + heartBtn.addEventListener('click', handleClickInactive); + } else { + heartBtn.addEventListener('pointerdown', handlePointerDown); + heartBtn.addEventListener('pointerup', handlePointerUp); + heartBtn.addEventListener('pointercancel', cancelLongPress); + heartBtn.addEventListener('pointerleave', cancelLongPress); + heartBtn.addEventListener('click', (e) => e.preventDefault()); + } + } + updateHeartListeners(); + const origSetAttr = heartBtn.setAttribute.bind(heartBtn); + heartBtn.setAttribute = (name, val) => { origSetAttr(name, val); if (name === 'data-favorited') updateHeartListeners(); }; + + // Expand arrow event + const arrowBtn = card.querySelector('.expand-arrow'); + arrowBtn.addEventListener('click', (e) => { + e.stopPropagation(); + card.classList.toggle('expanded'); + if (card.classList.contains('expanded')) { + card.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + }); + + // Card click navigation (exclude heart, arrow, chat) + card.addEventListener('click', (e) => { + if (!e.target.closest('.favorite-heart') && !e.target.closest('.expand-arrow') && !e.target.closest('.chat-icon')) { + window.location.href = `/anime/${show.mal_id}`; + } + }); + + return card; +} + +// Escape HTML +function escapeHtml(text) { + if (!text) return ''; + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} \ No newline at end of file diff --git a/frontend/static/js/chatbot/chat_utils.js b/frontend/static/js/chatbot/chat_utils.js index 9bee894..e2c5bd0 100644 --- a/frontend/static/js/chatbot/chat_utils.js +++ b/frontend/static/js/chatbot/chat_utils.js @@ -1,5 +1,5 @@ import { postError, postErrorMessage } from "../error.js" -import { pushMessages, pullMessages } from "./chat_history_db.js"; +import { pushMessages, pullMessages } from "../localDBs/chatHistoryDB.js"; // Client-side conversation message storage (Chats are only stored on client side for now) export let messages = []; diff --git a/frontend/static/js/favorites/favorites.js b/frontend/static/js/favorites/favorites.js index be9661b..7efa86c 100644 --- a/frontend/static/js/favorites/favorites.js +++ b/frontend/static/js/favorites/favorites.js @@ -1,246 +1,114 @@ import { renderPagination } from '../pagination.js'; -import { postErrorMessage } from '../error.js'; +import { renderAnimeCard } from '../animecard.js'; +import { getFavorites } from '../localDBs/favoritesDB.js' -// Configuration -const FAVORITES_RETRIEVE_POINT = "/proxy/anizenith/search"; -const ITEMS_PER_PAGE = 4; +// ===== Configuration & State ===== +const ITEMS_PER_PAGE = 8; let currentPage = 1; -let favorites = []; let filteredFavorites = []; let sortOption = 'dateAdded'; let searchTerm = ''; -// DOM elements +// ===== DOM Elements ===== const $ = id => document.getElementById(id); -const gridEl = $('favoritesGrid'); -const loadingEl = $('favoritesLoading'); -const emptyStateEl = $('emptyState'); -const paginationEl = $('pagination'); +const gridEl = $('results-container'); +const noResultsTemplate = $('tmpl-no-results'); +const paginationEl = $('pagination-wrapper'); const sortSelect = $('sortSelect'); const searchInput = $('searchFavoritesInput'); const clearSearchBtn = $('clearSearchBtn'); -const cardTemplate = $('anime-card-template'); - -// Fetch favorites from backend -async function fetchFavorites() { - try { - // TODO: Remove this and rely solely on browser loading, this is purely for testing - const params = new URLSearchParams({ idx_from: '0', idx_to: '25' }); - const response = await fetch(`${FAVORITES_RETRIEVE_POINT}?${params}`); - - if (!response.ok) { - // Custom error reporting (passes HTTP status, user message, endpoint) - postErrorMessage(response.status, "Could not fetch Favorites", FAVORITES_RETRIEVE_POINT); - return []; - } - - const data = await response.json(); - // Cache the fresh data locally so it's available offline - localStorage.setItem('anizenith_favorites', JSON.stringify(data.shows)); - return data.shows; - - } catch (error) { - console.error('Failed to fetch favorites from API:', error); - - // If network/other error, try to serve cached favorites - const cached = localStorage.getItem('anizenith_favorites'); - if (cached) { - return JSON.parse(cached); - } - return []; - } -} - -// Save favorites to local browser storage -function saveFavorites(favs) { - localStorage.setItem('anizenith_favorites', JSON.stringify(favs)); -} - -// Removes a single favorite anime by its ID -async function removeFavorite(animeId) { - const index = favorites.findIndex(a => a.id === animeId); - if (index !== -1) { - const animeTitle = favorites[index].name; - favorites.splice(index, 1); - saveFavorites(favorites); - - // Notify the user (custom status 102 means "Removed Favorite Anime") - postErrorMessage(102, "Removed Anime", `Removed "${animeTitle}" from favorites`); - applyFiltersAndRender(); - } -} - +const loadingEl = $('favoritesLoading'); -// Lambda expressions for comparing anime favorites stored on browser +// Sorting functions const sortFunctions = { - titleAsc: (a, b) => a.name.localeCompare(b.name), - titleDesc: (a, b) => b.name.localeCompare(a.name), - score: (a, b) => (b.score || 0) - (a.score || 0), - dateAdded: (a, b) => (b.date_added || 0) - (a.date_added || 0) + titleAsc: (a, b) => a.name.localeCompare(b.name), + titleDesc: (a, b) => b.name.localeCompare(a.name), + score: (a, b) => (b.score || 0) - (a.score || 0), + dateAdded: (a, b) => (b.date_added || 0) - (a.date_added || 0) }; -// Apply search and sort -function applyFilters() { - let result = [...favorites]; - - // Filter by search term - if (searchTerm.trim()) { - const term = searchTerm.toLowerCase(); - result = result.filter(anime => anime.name.toLowerCase().includes(term)); - } - - // Sort using the appropriate function, falling back to dateAdded - result.sort(sortFunctions[sortOption] || sortFunctions.dateAdded); - - filteredFavorites = result; - return result; -} - -function createAnimeCard(anime) { - // Clone template from HTML to build anime card - const clone = cardTemplate.content.cloneNode(true); - const card = clone.querySelector('.anime-card'); - const img = clone.querySelector('img'); - const heartBtn = clone.querySelector('.favorite-heart'); - const titleEl = clone.querySelector('.card-title'); - const ratingSpan = clone.querySelector('.rating-value'); - - // Populate the card with anime data - card.dataset.animeId = anime.mal_id; - img.src = anime.cover_image_url; - img.alt = anime.name; - heartBtn.dataset.id = anime.mal_id; - titleEl.textContent = anime.name; - titleEl.title = anime.name; - ratingSpan.textContent = anime.score?.toFixed(1) ?? 'N/A'; - - // Hold to remove feature: A long press (800ms) triggers removal, a short press does nothing. - const handlePointerDown = (e) => { - e.preventDefault(); // prevent selecting the text or area behind - heartBtn.classList.add('holding'); - heartBtn.holdTimer = setTimeout(() => removeFavorite(anime.mal_id), 800); - }; - - const handlePointerUp = () => { - clearTimeout(heartBtn.holdTimer); - heartBtn.classList.remove('holding'); - }; - - // Hold events for different devices - heartBtn.addEventListener('pointerdown', handlePointerDown); - heartBtn.addEventListener('pointerup', handlePointerUp); - heartBtn.addEventListener('pointercancel', handlePointerUp); - heartBtn.addEventListener('pointerleave', handlePointerUp); - - // Prevent the heart button click from triggering card navigation - heartBtn.addEventListener('click', (e) => e.preventDefault()); - - // Clicking anywhere on the card (except the heart) navigates to the anime's page - card.addEventListener('click', (e) => { - if (!e.target.closest('.favorite-heart')) { - window.location.href = `/anime/${anime.mal_id}`; - } - }); - - return card; +function applyFiltersAndSort() { + let favorites = getFavorites(); + let result = [...favorites]; + if (searchTerm.trim()) { + const term = searchTerm.toLowerCase(); + result = result.filter(anime => anime.name.toLowerCase().includes(term)); + } + result.sort(sortFunctions[sortOption] || sortFunctions.dateAdded); + filteredFavorites = result; + return result; } -// Render current page +// Render current page using the shared card component function renderPage() { - const filtered = filteredFavorites; - const totalItems = filtered.length; - const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE); - - // No results - show empty state, hide grid and pagination - if (totalItems === 0) { - gridEl.style.display = 'none'; - paginationEl.style.display = 'none'; - emptyStateEl.style.display = 'block'; - return; + const totalItems = filteredFavorites.length; + const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE); + + if (totalItems === 0) { + // If no favorites, show no-results template, hide pagination + paginationEl.style.display = 'none'; + gridEl.replaceChildren(noResultsTemplate.content.cloneNode(true)); + return; + } + + // Clear current grid items, add new ones + gridEl.replaceChildren(); + paginationEl.style.display = 'flex'; + + const start = (currentPage - 1) * ITEMS_PER_PAGE; + const end = Math.min(start + ITEMS_PER_PAGE, totalItems); + const pageItems = filteredFavorites.slice(start, end); + + pageItems.forEach(anime => { + const card = renderAnimeCard(anime); + gridEl.appendChild(card); + }); + + renderPagination(paginationEl, { + currentPage, + totalPages, + onPageChange: (newPage) => { + currentPage = newPage; + renderPage(); + window.scrollTo({ top: 0, behavior: 'smooth' }); } - - // Show the grid, hide empty state - emptyStateEl.style.display = 'none'; - gridEl.style.display = 'grid'; - - // Slice the visible page of items - const start = (currentPage - 1) * ITEMS_PER_PAGE; - const end = Math.min(start + ITEMS_PER_PAGE, totalItems); - const pageItems = filtered.slice(start, end); - - // Clear previous cards and append new ones - gridEl.innerHTML = ''; - pageItems.forEach(anime => { - gridEl.appendChild(createAnimeCard(anime)); - }); - - // Register and render pagination - renderPagination(paginationEl, { - currentPage, - totalPages, - onPageChange: (newPage) => { // When page is changed, set new page and render the page - currentPage = newPage; - renderPage(); - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - }); - - paginationEl.style.display = 'flex'; + }); } -// Apply filters and re-render -function applyFiltersAndRender() { - currentPage = 1; - applyFilters(); - renderPage(); +function refresh() { + applyFiltersAndSort(); + renderPage(); + if (loadingEl) loadingEl.style.display = 'none'; } +// Listen for favorites change and refresh favorites list +window.addEventListener('favoritesUpdated', () => { + refresh(); +}); + document.addEventListener('DOMContentLoaded', () => { - // Apply filters if user types and then stops typing (for 300ms) - let searchTimeout; - searchInput.addEventListener('input', (e) => { - clearTimeout(searchTimeout); - searchTimeout = setTimeout(() => { - searchTerm = e.target.value; - applyFiltersAndRender(); - }, 300); - }); - - // Clear search button resets the input and the results - clearSearchBtn.addEventListener('click', () => { - searchInput.value = ''; - searchTerm = ''; - applyFiltersAndRender(); - }); - - // Sort selector triggers a full re‑filter/re‑render - sortSelect.addEventListener('change', (e) => { - sortOption = e.target.value; - applyFiltersAndRender(); - }); - - // Main page init function: show loading animation, fetch data, then render (requires async due to fetch) - async function init() { - // Initial state: only the loading indicator is visible - emptyStateEl.style.display = 'none'; - gridEl.style.display = 'none'; - paginationEl.style.display = 'none'; - loadingEl.style.display = 'flex'; - - try { - // If fetch succeeds - favorites = await fetchFavorites(); - applyFilters(); - renderPage(); - } catch (error) { - // If backend is down / corrupted / other error, show empty state - postErrorMessage(500, "Load Failed", "Could not load favorites. Please try again."); - loadingEl.style.display = 'none'; - emptyStateEl.style.display = 'block'; - } finally { - loadingEl.style.display = 'none'; - } - } + let searchTimeout; + searchInput.addEventListener('input', (e) => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + searchTerm = e.target.value; + currentPage = 1; + refresh(); + }, 300); + }); + + clearSearchBtn.addEventListener('click', () => { + searchInput.value = ''; + searchTerm = ''; + currentPage = 1; + refresh(); + }); + + sortSelect.addEventListener('change', (e) => { + sortOption = e.target.value; + currentPage = 1; + refresh(); + }); - init(); + refresh(); }); \ No newline at end of file diff --git a/frontend/static/js/chatbot/chat_history_db.js b/frontend/static/js/localDBs/chatHistoryDB.js similarity index 100% rename from frontend/static/js/chatbot/chat_history_db.js rename to frontend/static/js/localDBs/chatHistoryDB.js diff --git a/frontend/static/js/localDBs/favoritesDB.js b/frontend/static/js/localDBs/favoritesDB.js new file mode 100644 index 0000000..759ee92 --- /dev/null +++ b/frontend/static/js/localDBs/favoritesDB.js @@ -0,0 +1,42 @@ +const STORAGE_KEY = 'anizenith_favorites'; + +function getStore() { + const raw = localStorage.getItem(STORAGE_KEY); + return raw ? JSON.parse(raw) : {}; +} + +function setStore(store) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(store)); +} + +export function getFavorites() { + return Object.values(getStore()); +} + +export function saveFavorites(favoritesArray) { + const store = {}; + favoritesArray.forEach(anime => { if (anime?.mal_id) store[anime.mal_id] = anime; }); + setStore(store); +} + +export function isAnimeFavorited(id) { + return getStore().hasOwnProperty(id); +} + +export function addFavorite(anime) { + const store = getStore(); + if (!store[anime.mal_id]) { + store[anime.mal_id] = anime; + setStore(store); + window.dispatchEvent(new CustomEvent('favoritesUpdated', { detail: Object.values(store) })); + } +} + +export function removeFavorite(id) { + const store = getStore(); + if (store[id]) { + delete store[id]; + setStore(store); + window.dispatchEvent(new CustomEvent('favoritesUpdated', { detail: Object.values(store) })); + } +} \ No newline at end of file diff --git a/frontend/static/js/search/search.js b/frontend/static/js/search/search.js index 6e0071b..b987535 100644 --- a/frontend/static/js/search/search.js +++ b/frontend/static/js/search/search.js @@ -1,9 +1,10 @@ import { renderPagination } from '../pagination.js'; import { postErrorMessage } from '../error.js'; +import { renderAnimeCard } from '../animecard.js'; // ===== Configuration & State ===== const API_SEARCH_URL = '/proxy/anizenith/search'; -const ITEMS_PER_PAGE = 5; +const ITEMS_PER_PAGE = 8; let currentPage = 1; let totalCount = 0; let totalPages = 1; @@ -12,7 +13,7 @@ let totalPages = 1; const $ = id => document.getElementById(id); const searchForm = $('search-form'); const searchQuery = $('search-query'); -const resultsContainer = $('search-results-container'); +const resultsContainer = $('results-container'); const paginationWrapper = $('pagination-wrapper'); const toggleBtn = $('filterToggleBtn'); const filterPanel = $('filterPanel'); @@ -167,7 +168,6 @@ async function performSearch() { console.log(url); try { - // TODO: Modify fetch url to include pagination params const timeout = 60.0 const res = await fetch(url, { "X-Request-Timeout": timeout.toString() }); if (!res.ok) postErrorMessage(res.status, "Backend Search Error", API_SEARCH_URL); @@ -190,58 +190,18 @@ async function performSearch() { } } -// Renders a row object template to show a short panel describing an anime show dynamically -function renderShowRow(show) { - const template = document.getElementById('tmpl-result-row'); - const row = template.content.cloneNode(true); - - // Cover image of anime - const img = row.querySelector('img'); - img.src = show.cover_image_url || ''; - img.alt = escapeHtml(show.name); - - // Anime title - const titleEl = row.querySelector('.col-title'); - titleEl.textContent = show.name; - titleEl.title = show.name; - - // Anime genre - const genres = Array.isArray(show.genres) ? show.genres.join(', ') : show.genres || ''; - const genresEl = row.querySelector('.col-genres'); - genresEl.textContent = genres; - genresEl.title = genres; - - // Anime short description - const descEl = row.querySelector('.col-desc'); - descEl.textContent = show.synopsis || ''; - - // Adds row where clicking opens the anime's page - const rowElement = row.querySelector('.result-row') - rowElement.style.cursor = 'pointer'; - rowElement.addEventListener('click', () => { - window.location.href = `/anime/${show.id}`; - }); - - return row; -} - // Renders all page results in the current page function renderResults({ shows = [] }) { - resultsContainer.innerHTML = ''; - paginationWrapper.innerHTML = ''; - - if (!shows.length) { - const noResults = document.getElementById('tmpl-no-results').content.cloneNode(true); - resultsContainer.appendChild(noResults); - return; - } - - const header = document.getElementById('tmpl-results-header').content.cloneNode(true); - resultsContainer.appendChild(header); - - shows.forEach(show => { - resultsContainer.appendChild(renderShowRow(show)); - }); + if (!shows.length) { + const noResults = document.getElementById('tmpl-no-results').content.cloneNode(true); + resultsContainer.appendChild(noResults); + return; + } + + shows.forEach(show => { + const card = renderAnimeCard(show); + resultsContainer.appendChild(card); + }); } // Update controller function for pages @@ -263,14 +223,6 @@ function changePage(delta) { resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' }); } -// Utility to escape HTML for elements that are not string-safe (e.g. images) -function escapeHtml(text) { - if (!text) return ''; - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - // Loads the filters on the page if a direct URL with query params is used (stateless) // TODO: Remove idx_from and idx_to parameters or include logic to support it function loadStateFromURL() { diff --git a/frontend/templates/components/anime-card-template.html b/frontend/templates/components/anime-card-template.html new file mode 100644 index 0000000..a339638 --- /dev/null +++ b/frontend/templates/components/anime-card-template.html @@ -0,0 +1,35 @@ + \ No newline at end of file diff --git a/frontend/templates/components/pagination-template.html b/frontend/templates/components/pagination-template.html new file mode 100644 index 0000000..b8e5baa --- /dev/null +++ b/frontend/templates/components/pagination-template.html @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/frontend/templates/favorites.html b/frontend/templates/favorites.html index 96b7a01..f929922 100644 --- a/frontend/templates/favorites.html +++ b/frontend/templates/favorites.html @@ -7,75 +7,62 @@ {% endblock %} {% block content %} -

- Favorite Anime -

-
-
- -
- - - -
- -
- - -
+

Favorite Anime

+ +
+
+
+ + +
+
+ + +
+
-
- -
-
-

Loading your favorites...

-
+
+ +
+
+

Loading your favorites...

+
- -
+ {% include 'components/anime-card-template.html' %} + {% include 'components/pagination-template.html' %} - -
-
- -
-

Your favorites list is empty

-

Start adding anime you love and they'll appear here.

- - - Browse Anime - + + + +
+
+
- - +
+ +
- - {% endblock %} {% block scripts %} diff --git a/frontend/templates/search.html b/frontend/templates/search.html index 5c1fe52..e6b4ba9 100644 --- a/frontend/templates/search.html +++ b/frontend/templates/search.html @@ -11,162 +11,111 @@

Anienith Anime Search

-
-
-
-
- - - +
+
+ +
+ + + - -
+ +
-
-
- -
-

Genres

-
- - - - - - -
+
+
+ +
+

Genres

+
+ + + + + +
+
- -
-

Year Range

-
-
- - - -
-
+ +
+

Year Range

+
+
+ + +
+
+
- -
-

Score

-
-
- - - -
-
+ +
+

Score

+
+
+ + +
-
- - -
-

Status

- +
-
- + +
+

Status

+
- -
- - - - - - - - + +
- - -
-
-
-

Searching anime...

-
+ {% include 'components/anime-card-template.html' %} + {% include 'components/pagination-template.html' %} -
- -
+
+
+
+

Searching anime...

-
- + +
+
+
+ +
+ +
{% endblock %} From 0a06a045760a09ff8a39274ad81452da3da9e691 Mon Sep 17 00:00:00 2001 From: ShafathZ Date: Wed, 27 May 2026 13:29:37 -0400 Subject: [PATCH 7/9] Bug fix appending infinite search results, moved titles a bit higher over icons, removed some genres which are too long or do not contain useful meaning from displaying on cards --- frontend/static/css/animegrid.css | 10 +++++++++- frontend/static/js/animecard.js | 5 ++++- frontend/static/js/search/search.js | 24 ++++++++++++++---------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/frontend/static/css/animegrid.css b/frontend/static/css/animegrid.css index 62b8783..0db7473 100644 --- a/frontend/static/css/animegrid.css +++ b/frontend/static/css/animegrid.css @@ -98,7 +98,7 @@ .card-title { font-weight: 700; font-size: 0.85rem; - margin: 0 0 4px 0; + margin: 0 0 12px 0; white-space: normal; word-break: break-word; overflow: visible; @@ -271,6 +271,14 @@ border-left-color: var(--border-soft); } +.expand-arrow i { + transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + +.anime-card.expanded .expand-arrow i { + transform: rotate(-180deg); +} + .expand-info p { margin: 8px 0; font-size: 0.85rem; diff --git a/frontend/static/js/animecard.js b/frontend/static/js/animecard.js index 4c8c8c8..f921354 100644 --- a/frontend/static/js/animecard.js +++ b/frontend/static/js/animecard.js @@ -1,6 +1,9 @@ import { getFavorites, saveFavorites, isAnimeFavorited, addFavorite, removeFavorite } from './localDBs/favoritesDB.js' import { postErrorMessage } from './error.js'; +// Genres that are not to be displayed on front card icon +const excludedGenres = ["Award Winning", "Adult Cast", "Gore", "Parody", "Team Sports", "High Stakes Game", "Urban Fantasy"]; + // Template cloning and base setup export function renderAnimeCard(show) { const template = document.getElementById('tmpl-anime-card'); @@ -13,7 +16,7 @@ export function renderAnimeCard(show) { // Genre pills const genresContainer = card.querySelector('.card-genres'); - show.genres.slice(0, 3).forEach(genre => { + show.genres.filter(genre => !excludedGenres.includes(genre)).slice(0, 3).forEach(genre => { const pill = document.createElement('span'); pill.className = 'genre-pill'; pill.textContent = genre; diff --git a/frontend/static/js/search/search.js b/frontend/static/js/search/search.js index b987535..ae66db6 100644 --- a/frontend/static/js/search/search.js +++ b/frontend/static/js/search/search.js @@ -192,16 +192,20 @@ async function performSearch() { // Renders all page results in the current page function renderResults({ shows = [] }) { - if (!shows.length) { - const noResults = document.getElementById('tmpl-no-results').content.cloneNode(true); - resultsContainer.appendChild(noResults); - return; - } - - shows.forEach(show => { - const card = renderAnimeCard(show); - resultsContainer.appendChild(card); - }); + if (!shows.length) { + const noResults = document.getElementById('tmpl-no-results').content.cloneNode(true); + resultsContainer.appendChild(noResults); + return; + } + + // Clear children + resultsContainer.replaceChildren(); + + // Add new shows as cards + shows.forEach(show => { + const card = renderAnimeCard(show); + resultsContainer.appendChild(card); + }); } // Update controller function for pages From ab6ac8ace02574166b02a06f9b96d37d51567757 Mon Sep 17 00:00:00 2001 From: ShafathZ Date: Fri, 5 Jun 2026 01:58:47 -0400 Subject: [PATCH 8/9] Minor UI flow improvements. Fixed some layout grouping issues --- frontend/static/css/animegrid.css | 26 ++++++++++++----- frontend/static/css/search/search.css | 29 +++++++++++++++++++ frontend/static/js/search/search.js | 7 +++++ .../components/anime-card-template.html | 16 +++++----- frontend/templates/search.html | 10 +++++-- 5 files changed, 70 insertions(+), 18 deletions(-) diff --git a/frontend/static/css/animegrid.css b/frontend/static/css/animegrid.css index 0db7473..a917136 100644 --- a/frontend/static/css/animegrid.css +++ b/frontend/static/css/animegrid.css @@ -2,10 +2,14 @@ .results-container { display: flex; flex-wrap: wrap; - gap: 13px; + gap: 16px; justify-content: flex-start; align-items: stretch; - padding: 10px 0; + padding: 15px 0; +} + +.results-container.few-cards { + justify-content: flex-start; } .results-wrapper { @@ -87,7 +91,7 @@ padding: 0.8rem; background: var(--bg-glass-strong); background: linear-gradient(to top, var(--bg-glass-strong) 0%, transparent 100%); - backdrop-filter: blur(2px); + backdrop-filter: blur(3px); color: var(--text-main); border-radius: 0 0 20px 20px; z-index: 1; @@ -98,15 +102,16 @@ .card-title { font-weight: 700; font-size: 0.85rem; - margin: 0 0 12px 0; + margin: 0 0 14px 0; white-space: normal; word-break: break-word; - overflow: visible; + overflow-wrap: break-word; line-height: 1.3; - max-height: 2.6em; - display: -webkit-box; - -webkit-line-clamp: 2; + display: block; + max-height: none; + -webkit-line-clamp: unset; -webkit-box-orient: vertical; + overflow: visible; } /* ===== Card Bottom Icons ===== */ @@ -271,6 +276,11 @@ border-left-color: var(--border-soft); } +.anime-card.expanded .card-image-area, +.anime-card.expanded .card-overlay { + border-radius: 20px 0 0 20px; +} + .expand-arrow i { transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); } diff --git a/frontend/static/css/search/search.css b/frontend/static/css/search/search.css index e9f91c3..1eb5619 100644 --- a/frontend/static/css/search/search.css +++ b/frontend/static/css/search/search.css @@ -333,4 +333,33 @@ .no-results { margin: 2rem auto; width: 100%; +} + +/* ===== Go to Favorites Button ===== */ +.bottom-controls { + display: flex; + justify-content: center; + position: relative; +} + +.favorited-btn { + position: absolute; + right: 0; + top: 12px; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 8px 24px; + background: var(--accent-gradient); + border-radius: 40px; + color: white; + font-weight: 600; + text-decoration: none; + border: none; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; +} + +.favorited-btn:hover { + box-shadow: 0 0 25px var(--accent-glow); } \ No newline at end of file diff --git a/frontend/static/js/search/search.js b/frontend/static/js/search/search.js index ae66db6..086abce 100644 --- a/frontend/static/js/search/search.js +++ b/frontend/static/js/search/search.js @@ -201,6 +201,13 @@ function renderResults({ shows = [] }) { // Clear children resultsContainer.replaceChildren(); + // Check if container layout needs to be updated in case of low results + if (shows.length < ITEMS_PER_PAGE) { + resultsContainer.classList.add('few-cards'); + } else { + resultsContainer.classList.remove('few-cards'); + } + // Add new shows as cards shows.forEach(show => { const card = renderAnimeCard(show); diff --git a/frontend/templates/components/anime-card-template.html b/frontend/templates/components/anime-card-template.html index a339638..b908d68 100644 --- a/frontend/templates/components/anime-card-template.html +++ b/frontend/templates/components/anime-card-template.html @@ -12,6 +12,14 @@ +
+
+ +
+
+ +
+
@@ -23,13 +31,5 @@
-
-
- -
-
- -
-
\ No newline at end of file diff --git a/frontend/templates/search.html b/frontend/templates/search.html index e6b4ba9..b55be5e 100644 --- a/frontend/templates/search.html +++ b/frontend/templates/search.html @@ -113,8 +113,14 @@

Status

-
- +
{% endblock %} From 5be579c394ab4cd2e9ad4b1a8d5d6c6da0b8674c Mon Sep 17 00:00:00 2001 From: ShafathZ Date: Fri, 5 Jun 2026 03:04:17 -0400 Subject: [PATCH 9/9] Upgraded description look and made link to chatbot functional and changed link to anime page to only occur when image is clicked instead of full card --- frontend/static/css/animegrid.css | 28 ++++++++++++++++--- frontend/static/js/animecard.js | 22 +++++++++------ .../components/anime-card-template.html | 1 + 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/frontend/static/css/animegrid.css b/frontend/static/css/animegrid.css index a917136..1e6599e 100644 --- a/frontend/static/css/animegrid.css +++ b/frontend/static/css/animegrid.css @@ -60,6 +60,13 @@ flex-shrink: 0; } +.card-image { + width: 100%; + height: 100%; + border-radius: 20px; + cursor: pointer; +} + .card-genres { position: absolute; top: 12px; @@ -142,6 +149,7 @@ transition: all 0.2s ease; box-shadow: var(--shadow-soft); pointer-events: auto; + cursor: pointer; } .favorite-heart { @@ -266,18 +274,21 @@ padding: 0; white-space: normal; word-break: break-word; - height: 100%; - overflow-y: auto; + flex: 1 1 0; } .anime-card.expanded .card-expand-panel { width: 190px; padding: 16px 12px; border-left-color: var(--border-soft); + cursor: default; + + overflow-y: auto; } .anime-card.expanded .card-image-area, -.anime-card.expanded .card-overlay { +.anime-card.expanded .card-overlay, +.anime-card.expanded .card-image { border-radius: 20px 0 0 20px; } @@ -302,5 +313,14 @@ } .expand-description { - font-size: 0.8rem; + font-family: 'Segoe UI', system-ui, sans-serif; + font-size: 0.85rem; + line-height: 1.5; + color: var(--text-muted); + text-align: left; + hyphens: auto; +} + +.expand-description .synopsis { + white-space: pre-line; } \ No newline at end of file diff --git a/frontend/static/js/animecard.js b/frontend/static/js/animecard.js index f921354..2f32c7e 100644 --- a/frontend/static/js/animecard.js +++ b/frontend/static/js/animecard.js @@ -11,8 +11,12 @@ export function renderAnimeCard(show) { card.setAttribute('data-id', show.mal_id); // Card image - const imageArea = card.querySelector('.card-image-area'); + const imageArea = card.querySelector('.card-image'); imageArea.style.backgroundImage = `url(${show.cover_image_url})`; + // Clicking on image sends to the anime page + imageArea.addEventListener('click', (e) => { + window.location.href = `/anime/${show.mal_id}`; + }); // Genre pills const genresContainer = card.querySelector('.card-genres'); @@ -37,7 +41,7 @@ export function renderAnimeCard(show) { const date = new Date(show.date_aired); airedSpan.textContent = date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); const synopsisSpan = card.querySelector('.synopsis'); - synopsisSpan.textContent = show.synopsis; + synopsisSpan.innerHTML = show.synopsis; // Favorite heart (add/remove with long press) const heartBtn = card.querySelector('.favorite-heart'); @@ -102,6 +106,13 @@ export function renderAnimeCard(show) { const origSetAttr = heartBtn.setAttribute.bind(heartBtn); heartBtn.setAttribute = (name, val) => { origSetAttr(name, val); if (name === 'data-favorited') updateHeartListeners(); }; + // Chat icon sends user to chatbot + const chatBtn = card.querySelector('.chat-icon'); + chatBtn.addEventListener('click', (e) => { + e.stopPropagation(); + window.location.href = `/?request-info-id=${show.mal_id}`; + }); + // Expand arrow event const arrowBtn = card.querySelector('.expand-arrow'); arrowBtn.addEventListener('click', (e) => { @@ -112,13 +123,6 @@ export function renderAnimeCard(show) { } }); - // Card click navigation (exclude heart, arrow, chat) - card.addEventListener('click', (e) => { - if (!e.target.closest('.favorite-heart') && !e.target.closest('.expand-arrow') && !e.target.closest('.chat-icon')) { - window.location.href = `/anime/${show.mal_id}`; - } - }); - return card; } diff --git a/frontend/templates/components/anime-card-template.html b/frontend/templates/components/anime-card-template.html index b908d68..d3b8e89 100644 --- a/frontend/templates/components/anime-card-template.html +++ b/frontend/templates/components/anime-card-template.html @@ -1,6 +1,7 @@