From 1bd98ee4af890d21905b1f61d0fec12043a41abf Mon Sep 17 00:00:00 2001 From: Adam Schmidt Date: Sun, 25 Jan 2026 10:03:07 -0800 Subject: [PATCH] Add support for sorting by difficulty --- options.css | 20 ++++++ options.html | 17 +++++ options.js | 103 ++++++++++++++++++++++++++++++- popup.css | 25 ++++++++ popup.html | 27 +++++--- popup.js | 54 +++++++++++++++- src/background/messageHandler.js | 43 +++++++++---- src/background/problemLogic.js | 88 ++++++++++++++++++++++++-- 8 files changed, 347 insertions(+), 30 deletions(-) diff --git a/options.css b/options.css index f49a2a7..06e2289 100644 --- a/options.css +++ b/options.css @@ -428,6 +428,7 @@ input:focus + .slider { padding: 12px 16px; border-top: 1px solid #e5e7eb; transition: background 0.2s ease; + gap: 8px; } .problem-item:hover { @@ -505,3 +506,22 @@ input:focus + .slider { color: #3b82f6; font-weight: bold; } + +.neetcode-video-link { + margin-left: 8px; + font-size: 16px; + text-decoration: none; + opacity: 0.7; + transition: opacity 0.2s ease, transform 0.2s ease; + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; +} + +.neetcode-video-link:hover { + opacity: 1; + transform: scale(1.1); +} + diff --git a/options.html b/options.html index ef25177..30252c7 100644 --- a/options.html +++ b/options.html @@ -66,6 +66,23 @@

Display Preferences

+ +
+
+
+ +

+ Sort problems within each category by difficulty (Easy → Medium → Hard) instead of the original problemset order +

+
+ +
+
diff --git a/options.js b/options.js index ee3da0a..308fff3 100644 --- a/options.js +++ b/options.js @@ -1,5 +1,7 @@ // Leetcode Buddy - Options Page Script +const ALIASES_PATH = "src/assets/data/problemAliases.json"; + const problemSetSelect = document.getElementById("problemSetSelect"); const totalProblems = document.getElementById("totalProblems"); const totalCategories = document.getElementById("totalCategories"); @@ -9,6 +11,65 @@ const resetConfirm = document.getElementById("resetConfirm"); const confirmReset = document.getElementById("confirmReset"); const cancelReset = document.getElementById("cancelReset"); const celebrationToggle = document.getElementById("celebrationToggle"); +const sortByDifficultyToggle = document.getElementById("sortByDifficultyToggle"); + +// Load aliases for NeetCode URL resolution +let problemAliases = {}; + +async function loadAliases() { + try { + const response = await fetch(chrome.runtime.getURL(ALIASES_PATH)); + problemAliases = await response.json(); + return problemAliases; + } catch (error) { + console.error("Failed to load aliases:", error); + return {}; + } +} + +// Find alias for a canonical slug (reverse lookup) +function findAliasForSlug(canonicalSlug) { + for (const [alias, canonical] of Object.entries(problemAliases)) { + if (canonical === canonicalSlug) { + return alias; + } + } + return null; +} + +// Get NeetCode slug for URL +function getNeetCodeSlug(slug) { + // First check if the slug itself is an alias + if (problemAliases[slug]) { + return slug; + } + + // Check if there's an alias for this canonical slug + const alias = findAliasForSlug(slug); + if (alias) { + return alias; + } + + // No alias found, use original slug + return slug; +} + +// Get NeetCode solution URL +function getNeetCodeUrl(slug) { + const neetcodeSlug = getNeetCodeSlug(slug); + return `https://neetcode.io/solutions/${neetcodeSlug}`; +} + +// Sort problems by difficulty +function sortProblemsByDifficulty(problems) { + const difficultyOrder = { Easy: 0, Medium: 1, Hard: 2 }; + + return [...problems].sort((a, b) => { + const diffA = difficultyOrder[a.difficulty] ?? 999; + const diffB = difficultyOrder[b.difficulty] ?? 999; + return diffA - diffB; + }); +} // Load current settings async function loadSettings() { @@ -24,7 +85,8 @@ async function loadSettings() { // Load selected problem set const result = await chrome.storage.sync.get([ "selectedProblemSet", - "celebrationEnabled" + "celebrationEnabled", + "sortByDifficulty" ]); const selectedSet = result.selectedProblemSet || "neetcode250"; problemSetSelect.value = selectedSet; @@ -32,6 +94,10 @@ async function loadSettings() { // Load celebration toggle setting (default: true) const celebrationEnabled = result.celebrationEnabled !== false; celebrationToggle.checked = celebrationEnabled; + + // Load sort by difficulty toggle setting (default: false) + const sortByDifficulty = result.sortByDifficulty === true; + sortByDifficultyToggle.checked = sortByDifficulty; } } catch (error) { console.error("Failed to load settings:", error); @@ -104,6 +170,27 @@ celebrationToggle.addEventListener("change", async () => { } }); +// Handle sort by difficulty toggle +sortByDifficultyToggle.addEventListener("change", async () => { + const enabled = sortByDifficultyToggle.checked; + + try { + await chrome.storage.sync.set({ sortByDifficulty: enabled }); + console.log("Sort by difficulty:", enabled ? "enabled" : "disabled"); + + // Recompute next problem to reflect the new ordering + await chrome.runtime.sendMessage({ type: "REFRESH_STATUS" }); + + // Re-render category accordion with new sorting + await renderCategoryAccordion(); + + // Reload settings to show updated current problem + await loadSettings(); + } catch (error) { + console.error("Failed to save sort by difficulty setting:", error); + } +}); + // Render category accordion async function renderCategoryAccordion() { const container = document.getElementById('categoryAccordion'); @@ -161,7 +248,7 @@ function createCategoryAccordionItem(category) { const content = document.createElement('div'); content.className = 'category-content'; - // Add problems + // Add problems (already sorted by message handler if needed) category.problems.forEach(problem => { const problemDiv = document.createElement('div'); problemDiv.className = `problem-item ${problem.isCurrent ? 'problem-current' : ''}`; @@ -184,9 +271,18 @@ function createCategoryAccordionItem(category) { difficultyDiv.className = `problem-difficulty difficulty-${problem.difficulty.toLowerCase()}`; difficultyDiv.textContent = problem.difficulty; + // Add NeetCode video icon + const videoLink = document.createElement('a'); + videoLink.href = getNeetCodeUrl(problem.slug); + videoLink.target = '_blank'; + videoLink.className = 'neetcode-video-link'; + videoLink.title = 'View NeetCode solution'; + videoLink.innerHTML = '▶️'; + problemDiv.appendChild(statusDiv); problemDiv.appendChild(titleDiv); problemDiv.appendChild(difficultyDiv); + problemDiv.appendChild(videoLink); content.appendChild(problemDiv); }); @@ -204,7 +300,8 @@ function createCategoryAccordionItem(category) { } // Initialize on load -document.addEventListener("DOMContentLoaded", () => { +document.addEventListener("DOMContentLoaded", async () => { + await loadAliases(); loadSettings(); renderCategoryAccordion(); }); diff --git a/popup.css b/popup.css index 0e4db42..76d17aa 100644 --- a/popup.css +++ b/popup.css @@ -161,6 +161,13 @@ header h1 { color: #991b1b; } +.problem-links { + display: flex; + align-items: center; + gap: 12px; + margin-top: 8px; +} + .problem-link { display: inline-block; color: #667eea; @@ -174,6 +181,24 @@ header h1 { color: #764ba2; } +.neetcode-video-link { + font-size: 18px; + text-decoration: none; + opacity: 0.7; + transition: opacity 0.2s ease, transform 0.2s ease; + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + line-height: 1; +} + +.neetcode-video-link:hover { + opacity: 1; + transform: scale(1.15); +} + .category-stats-section { padding: 24px 20px; border-bottom: 1px solid #e5e7eb; diff --git a/popup.html b/popup.html index 7836b9b..f62062d 100644 --- a/popup.html +++ b/popup.html @@ -39,14 +39,25 @@

Current Problem:

Arrays & Hashing
Loading...
Medium
- - Open on LeetCode → - + diff --git a/popup.js b/popup.js index 8e59ef1..f9fb3c4 100644 --- a/popup.js +++ b/popup.js @@ -1,5 +1,7 @@ // Leetcode Buddy - Popup Script with Category Stats +const ALIASES_PATH = "src/assets/data/problemAliases.json"; + // DOM elements const progressFill = document.getElementById("progressFill"); const solvedCount = document.getElementById("solvedCount"); @@ -8,7 +10,55 @@ const currentProblemName = document.getElementById("currentProblemName"); const currentCategory = document.getElementById("currentCategory"); const currentDifficulty = document.getElementById("currentDifficulty"); const currentProblemLink = document.getElementById("currentProblemLink"); +const neetcodeVideoLink = document.getElementById("neetcodeVideoLink"); const dailyStatus = document.getElementById("dailyStatus"); + +// Load aliases for NeetCode URL resolution +let problemAliases = {}; + +async function loadAliases() { + try { + const response = await fetch(chrome.runtime.getURL(ALIASES_PATH)); + problemAliases = await response.json(); + return problemAliases; + } catch (error) { + console.error("Failed to load aliases:", error); + return {}; + } +} + +// Find alias for a canonical slug (reverse lookup) +function findAliasForSlug(canonicalSlug) { + for (const [alias, canonical] of Object.entries(problemAliases)) { + if (canonical === canonicalSlug) { + return alias; + } + } + return null; +} + +// Get NeetCode slug for URL +function getNeetCodeSlug(slug) { + // First check if the slug itself is an alias + if (problemAliases[slug]) { + return slug; + } + + // Check if there's an alias for this canonical slug + const alias = findAliasForSlug(slug); + if (alias) { + return alias; + } + + // No alias found, use original slug + return slug; +} + +// Get NeetCode solution URL +function getNeetCodeUrl(slug) { + const neetcodeSlug = getNeetCodeSlug(slug); + return `https://neetcode.io/solutions/${neetcodeSlug}`; +} const categoryList = document.getElementById("categoryList"); const toggleCategories = document.getElementById("toggleCategories"); const bypassActive = document.getElementById("bypassActive"); @@ -118,6 +168,7 @@ async function updateStatus() { problem.difficulty )}`; currentProblemLink.href = `https://leetcode.com/problems/${problem.slug}/`; + neetcodeVideoLink.href = getNeetCodeUrl(problem.slug); } // Update daily solve status with celebration animation @@ -285,7 +336,8 @@ optionsButton.addEventListener("click", () => { }); // Initialize on load -document.addEventListener("DOMContentLoaded", () => { +document.addEventListener("DOMContentLoaded", async () => { + await loadAliases(); updateStatus(); // Refresh status every 30 seconds diff --git a/src/background/messageHandler.js b/src/background/messageHandler.js index b625d6f..3cb7a9b 100644 --- a/src/background/messageHandler.js +++ b/src/background/messageHandler.js @@ -169,19 +169,36 @@ async function handleGetDetailedProgress() { const problemSet = problemLogic.getProblemSet(); const state = await storage.getState(); - const categories = problemSet.categories.map((cat, catIdx) => ({ - name: cat.name, - total: cat.problems.length, - solved: cat.problems.filter(p => state.solvedProblems.has(p.slug)).length, - problems: cat.problems.map((p, probIdx) => ({ - slug: p.slug, - title: p.title, - difficulty: p.difficulty, - solved: state.solvedProblems.has(p.slug), - isCurrent: catIdx === state.currentCategoryIndex && - probIdx === state.currentProblemIndex - })) - })); + // Get current problem slug for isCurrent comparison + const currentCategory = problemSet.categories[state.currentCategoryIndex]; + const currentProblem = currentCategory?.problems[state.currentProblemIndex]; + const currentProblemSlug = currentProblem?.slug || null; + + // Check if sorting by difficulty is enabled + const settings = await chrome.storage.sync.get(['sortByDifficulty']); + const sortByDifficulty = settings.sortByDifficulty === true; + + const categories = problemSet.categories.map((cat, catIdx) => { + // Get problems, sorted if needed + let problems = cat.problems; + if (sortByDifficulty) { + problems = problemLogic.sortProblemsByDifficulty(cat.problems); + } + + return { + name: cat.name, + total: cat.problems.length, + solved: cat.problems.filter(p => state.solvedProblems.has(p.slug)).length, + problems: problems.map((p) => ({ + slug: p.slug, + title: p.title, + difficulty: p.difficulty, + solved: state.solvedProblems.has(p.slug), + // Use slug-based comparison for isCurrent to work with sorting + isCurrent: p.slug === currentProblemSlug + })) + }; + }); return { success: true, categories }; } diff --git a/src/background/problemLogic.js b/src/background/problemLogic.js index c338ec4..6579be5 100644 --- a/src/background/problemLogic.js +++ b/src/background/problemLogic.js @@ -67,6 +67,68 @@ export function resolveProblemAlias(slug) { return problemAliases[slug] || slug; } +/** + * Find alias for a canonical slug (reverse lookup) + * @param {string} canonicalSlug - Canonical problem slug + * @returns {string|null} Alias if found, null otherwise + */ +function findAliasForSlug(canonicalSlug) { + for (const [alias, canonical] of Object.entries(problemAliases)) { + if (canonical === canonicalSlug) { + return alias; + } + } + return null; +} + +/** + * Get the slug to use for NeetCode URL + * Uses alias if available, otherwise uses the original slug + * @param {string} slug - Problem slug (canonical or alias) + * @returns {string} Slug to use for NeetCode URL + */ +export function getNeetCodeSlug(slug) { + // First check if the slug itself is an alias + if (problemAliases[slug]) { + // It's an alias, use it directly + return slug; + } + + // Check if there's an alias for this canonical slug + const alias = findAliasForSlug(slug); + if (alias) { + return alias; + } + + // No alias found, use original slug + return slug; +} + +/** + * Get NeetCode solution URL for a problem + * @param {string} slug - Problem slug + * @returns {string} NeetCode solution URL + */ +export function getNeetCodeUrl(slug) { + const neetcodeSlug = getNeetCodeSlug(slug); + return `https://neetcode.io/solutions/${neetcodeSlug}`; +} + +/** + * Sort problems by difficulty (Easy → Medium → Hard) + * @param {Array} problems - Array of problem objects + * @returns {Array} Sorted array of problems + */ +export function sortProblemsByDifficulty(problems) { + const difficultyOrder = { Easy: 0, Medium: 1, Hard: 2 }; + + return [...problems].sort((a, b) => { + const diffA = difficultyOrder[a.difficulty] ?? 999; + const diffB = difficultyOrder[b.difficulty] ?? 999; + return diffA - diffB; + }); +} + /** * Fetch all problem statuses from LeetCode API * @returns {Promise} Map of problem slug to status @@ -161,25 +223,41 @@ export async function computeNextProblem(syncAllSolved = false) { } } + // Check if sorting by difficulty is enabled + const settings = await chrome.storage.sync.get(['sortByDifficulty']); + const sortByDifficulty = settings.sortByDifficulty === true; + // Second pass: Find first unsolved problem in order for (let catIdx = 0; catIdx < problemSet.categories.length; catIdx++) { const category = problemSet.categories[catIdx]; - for (let probIdx = 0; probIdx < category.problems.length; probIdx++) { - const problem = category.problems[probIdx]; + // Get problems, sorted if needed + let problemsToCheck = category.problems; + if (sortByDifficulty) { + problemsToCheck = sortProblemsByDifficulty(category.problems); + } + + for (let probIdx = 0; probIdx < problemsToCheck.length; probIdx++) { + const problem = problemsToCheck[probIdx]; if (!solvedProblems.has(problem.slug)) { // Found first unsolved problem + // Find original index if sorted + let originalProbIdx = probIdx; + if (sortByDifficulty) { + originalProbIdx = category.problems.findIndex(p => p.slug === problem.slug); + } + currentCategoryIndex = catIdx; - currentProblemIndex = probIdx; + currentProblemIndex = originalProbIdx; currentProblemSlug = problem.slug; - await saveState(catIdx, probIdx, solvedProblems); + await saveState(catIdx, originalProbIdx, solvedProblems); return { categoryIndex: catIdx, categoryName: category.name, - problemIndex: probIdx, + problemIndex: originalProbIdx, problem: problem, totalProblems: problemSet.categories.reduce( (sum, cat) => sum + cat.problems.length,