From 74e2888c09a2ee9173b4b6c1afe3554cc4a755fd Mon Sep 17 00:00:00 2001
From: Horrid-12 <96373496+Horrid-12@users.noreply.github.com~>
Date: Sun, 21 Jun 2026 20:02:55 +0530
Subject: [PATCH 1/2] feat: theme toggle, top anime mode, recommendations
---
anime.js | 103 +++++++++++++++++++++++++++++++++++++++--
src/style.css | 1 +
updates.html | 126 +++++++++++++++++++++++++++-----------------------
3 files changed, 169 insertions(+), 61 deletions(-)
diff --git a/anime.js b/anime.js
index da8588b..24ea389 100644
--- a/anime.js
+++ b/anime.js
@@ -39,8 +39,10 @@ const viewSeasonal = document.getElementById("view-seasonal");
const viewWeekly = document.getElementById("view-weekly");
const viewMonthly = document.getElementById("view-monthly");
const viewYearly = document.getElementById("view-yearly");
+const viewTop = document.getElementById("view-top");
const pageHeading = document.getElementById("page-heading");
const pageSubheading = document.getElementById("page-subheading");
+const themeToggle = document.getElementById("theme-toggle");
// --- Accessibility upgrades (safe if missing) ---
if (resultsInfo) resultsInfo.setAttribute("aria-live", "polite");
@@ -95,6 +97,24 @@ function safeParse(value, fallback) {
}
}
+// Theme toggle
+function setTheme(mode) {
+ const html = document.documentElement;
+ if (mode === "light") {
+ html.classList.add("light");
+ if (themeToggle) themeToggle.textContent = "🌙";
+ } else {
+ html.classList.remove("light");
+ if (themeToggle) themeToggle.textContent = "☀️";
+ }
+ localStorage.setItem("theme", mode);
+}
+
+function toggleTheme() {
+ const isLight = document.documentElement.classList.contains("light");
+ setTheme(isLight ? "dark" : "light");
+}
+
// Persist filters
function saveFilters() {
const filters = {
@@ -131,11 +151,12 @@ function updateFilterVisibility() {
const isWeekly = currentViewMode === "weekly";
const isMonthly = currentViewMode === "monthly";
const isYearly = currentViewMode === "yearly";
+ const isTop = currentViewMode === "top";
// Season select: visible in seasonal + yearly (for yearly mode it's the season picker)
seasonSelect?.closest("div")?.classList.toggle("hidden", !isSeasonal && !isYearly);
// Year select: visible in seasonal + monthly + yearly
- yearSelect?.closest("div")?.classList.toggle("hidden", isWeekly);
+ yearSelect?.closest("div")?.classList.toggle("hidden", isWeekly || isTop);
// Day filter: visible only in weekly mode
dayFilter?.classList.toggle("hidden", !isWeekly);
// Month filter: visible only in monthly mode
@@ -145,11 +166,11 @@ function updateFilterVisibility() {
function setViewMode(mode) {
currentViewMode = mode;
// update tab styles
- [viewSeasonal, viewWeekly, viewMonthly, viewYearly].forEach((btn) => {
+ [viewSeasonal, viewWeekly, viewMonthly, viewTop, viewYearly].forEach((btn) => {
btn?.classList.remove("bg-blue-500", "text-white");
btn?.classList.add("bg-zinc-700", "text-zinc-300", "hover:bg-zinc-600");
});
- const activeBtn = { seasonal: viewSeasonal, weekly: viewWeekly, monthly: viewMonthly, yearly: viewYearly }[mode];
+ const activeBtn = { seasonal: viewSeasonal, weekly: viewWeekly, monthly: viewMonthly, yearly: viewYearly, top: viewTop }[mode];
activeBtn?.classList.remove("bg-zinc-700", "text-zinc-300", "hover:bg-zinc-600");
activeBtn?.classList.add("bg-blue-500", "text-white");
@@ -159,6 +180,7 @@ function setViewMode(mode) {
weekly: ["📅 Weekly Schedule", "Browse your favorite animes by day"],
monthly: ["📅 Monthly Anime", "Browse your favorite animes by month"],
yearly: ["📅 Yearly Anime", "Browse your favorite animes by year"],
+ top: ["🏆 Top Anime", "Browse the highest rated anime"],
};
const [h, s] = labels[mode] || labels.seasonal;
if (pageHeading) pageHeading.textContent = h;
@@ -336,6 +358,9 @@ function openModal(anime) {
// Favorites button
setUpFavButton(anime);
+ // Recommendations
+ loadRecommendations(anime.mal_id);
+
// Show modal + focus trap
previouslyFocusedEl = document.activeElement;
modal.classList.remove("hidden");
@@ -361,6 +386,70 @@ function closeModalFn() {
}, 300);
}
+const REC_CACHE_KEY_PREFIX = "jikan_rec_cache_";
+
+async function loadRecommendations(malId) {
+ const section = document.getElementById("modal-recommendations-section");
+ const container = document.getElementById("modal-recommendations");
+ if (!section || !container) return;
+
+ container.innerHTML = "";
+ const cacheKey = REC_CACHE_KEY_PREFIX + malId;
+ const cachedRaw = sessionStorage.getItem(cacheKey);
+ if (cachedRaw) {
+ const cached = safeParse(cachedRaw, {});
+ if (Date.now() - cached.time < CACHE_TTL && Array.isArray(cached.data)) {
+ renderRecommendations(cached.data, section, container);
+ return;
+ }
+ }
+
+ try {
+ const res = await fetch(`https://api.jikan.moe/v4/anime/${malId}/recommendations`);
+ if (!res.ok) { section.classList.add("hidden"); return; }
+ const json = await res.json();
+ const recs = (json.data || []).slice(0, 10);
+ sessionStorage.setItem(cacheKey, JSON.stringify({ time: Date.now(), data: recs }));
+ renderRecommendations(recs, section, container);
+ } catch {
+ section.classList.add("hidden");
+ }
+}
+
+function renderRecommendations(recs, section, container) {
+ section.classList.remove("hidden");
+ container.innerHTML = "";
+ recs.forEach((entry) => {
+ const anime = entry.entry;
+ if (!anime) return;
+ const card = document.createElement("div");
+ card.className = "flex-shrink-0 w-24 cursor-pointer hover:opacity-80 transition-opacity";
+ const img = document.createElement("img");
+ img.src = anime.images?.webp?.image_url || anime.images?.jpg?.image_url || "";
+ img.alt = anime.title || "";
+ img.loading = "lazy";
+ img.className = "w-full h-32 object-cover rounded";
+ const title = document.createElement("p");
+ title.className = "text-xs text-zinc-300 mt-1 truncate light:text-zinc-700";
+ title.textContent = anime.title || "";
+ card.appendChild(img);
+ card.appendChild(title);
+ card.addEventListener("click", () => openModalById(anime.mal_id));
+ container.appendChild(card);
+ });
+}
+
+function openModalById(malId) {
+ showLoading();
+ const controller = new AbortController();
+ const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
+ fetch(`https://api.jikan.moe/v4/anime/${malId}`, { signal: controller.signal })
+ .then(r => { clearTimeout(t); if (!r.ok) throw new Error(); return r.json(); })
+ .then(data => { if (data?.data) openModal(data.data); })
+ .catch(() => { hideLoading(); })
+ .finally(() => clearTimeout(t));
+}
+
// Favorites
function toggleFavorite(anime) {
const exists = favorites.some((f) => f.mal_id === anime.mal_id);
@@ -484,6 +573,7 @@ function renderFavorites() {
// Fetch with timeout + session cache
function getApiUrl() {
if (currentViewMode === "weekly") return "https://api.jikan.moe/v4/schedules";
+ if (currentViewMode === "top") return "https://api.jikan.moe/v4/top/anime";
if (currentViewMode === "yearly") {
const season = seasonSelect?.value || "";
const year = yearSelect?.value || "";
@@ -495,6 +585,7 @@ function getApiUrl() {
function getCacheKey() {
if (currentViewMode === "weekly") return "jikan_schedule_cache_v1";
+ if (currentViewMode === "top") return "jikan_top_cache_v1";
if (currentViewMode === "yearly") {
const season = seasonSelect?.value || "";
const year = yearSelect?.value || "";
@@ -802,8 +893,12 @@ document.addEventListener("keydown", (e) => {
viewSeasonal?.addEventListener("click", () => setViewMode("seasonal"));
viewWeekly?.addEventListener("click", () => setViewMode("weekly"));
viewMonthly?.addEventListener("click", () => setViewMode("monthly"));
+viewTop?.addEventListener("click", () => setViewMode("top"));
viewYearly?.addEventListener("click", () => setViewMode("yearly"));
+// Theme toggle
+themeToggle?.addEventListener("click", toggleTheme);
+
// Day / Month filters
daySelect?.addEventListener("change", () => { currentPage = 1; applyFilters(); });
monthSelect?.addEventListener("change", () => { currentPage = 1; applyFilters(); });
@@ -833,6 +928,8 @@ function populateYearFilter() {
}
// Initialize
+const savedTheme = localStorage.getItem("theme");
+if (savedTheme) setTheme(savedTheme);
populateYearFilter();
loadFilters();
renderFavorites();
diff --git a/src/style.css b/src/style.css
index f1d8c73..5ea2bf9 100644
--- a/src/style.css
+++ b/src/style.css
@@ -1 +1,2 @@
@import "tailwindcss";
+@custom-variant light (&:where(.light, .light *));
diff --git a/updates.html b/updates.html
index 0a826db..bfeeb86 100644
--- a/updates.html
+++ b/updates.html
@@ -7,22 +7,25 @@
-
+
-