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
+
+
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