Skip to content

Commit 9d98062

Browse files
committed
feat: pulling problem tags from the leetcode graphql api
1 parent 66d0282 commit 9d98062

File tree

5 files changed

+166
-60
lines changed

5 files changed

+166
-60
lines changed

content.js

Lines changed: 55 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,62 @@
1-
function getProblemData() {
1+
// Import fetchLeetCodeTags from leetcodeApi.js
2+
// (Assumes leetcodeApi.js is loaded before this script as per manifest)
3+
4+
async function getProblemData() {
25
const titleEl = document.querySelector('div[class*="text-title-large"]');
36
const title = titleEl ? titleEl.textContent.trim() : "Unknown Title";
4-
7+
58
const difficultyEl = Array.from(document.querySelectorAll('div[class*="text-difficulty"]')).find((el) =>
6-
el.textContent?.match(/Easy|Medium|Hard/)
9+
el.textContent?.match(/Easy|Medium|Hard/)
710
);
811
const difficulty = difficultyEl
9-
? difficultyEl.textContent.trim()
10-
: "Unknown Difficulty";
11-
12+
? difficultyEl.textContent.trim()
13+
: "Unknown Difficulty";
14+
1215
const pathParts = window.location.pathname.split("/").filter(Boolean);
1316
const problemSlug = pathParts[1] || "unknown-problem";
14-
17+
18+
// Fetch tags using the API helper
19+
let tags = [];
20+
try {
21+
tags = await fetchLeetCodeTags(problemSlug);
22+
} catch (e) {
23+
tags = [];
24+
}
25+
1526
const problemData = {
16-
title,
17-
difficulty,
18-
slug: problemSlug,
19-
url: window.location.href,
20-
timestamp: Date.now(),
27+
title,
28+
difficulty,
29+
slug: problemSlug,
30+
url: window.location.href,
31+
timestamp: Date.now(),
32+
tags,
2133
};
22-
34+
2335
console.log("LC Problem Detected:", problemData);
2436
return problemData;
25-
}
26-
27-
function waitForContentAndStore() {
28-
const observer = new MutationObserver(() => {
29-
const titleEl = document.querySelector("div");
30-
if (titleEl && titleEl.textContent.trim()) {
31-
observer.disconnect();
32-
const data = getProblemData();
33-
browser.storage.local.set({ [data.slug]: data }).then(() => {
34-
console.log("Saved to storage:", data);
35-
}).catch((err) => {
36-
console.error("Storage error:", err);
37-
});
38-
}
37+
}
38+
39+
// Wait for content and store problem data (with tags)
40+
function waitForContentAndStore() {
41+
const observer = new MutationObserver(async () => {
42+
const titleEl = document.querySelector("div");
43+
if (titleEl && titleEl.textContent.trim()) {
44+
observer.disconnect();
45+
const data = await getProblemData();
46+
browser.storage.local.set({ [data.slug]: data }).then(() => {
47+
console.log("Saved to storage:", data);
48+
}).catch((err) => {
49+
console.error("Storage error:", err);
50+
});
51+
// Start watching for submission result after we have the slug
52+
waitForSubmissionResult(data.slug);
53+
}
3954
});
40-
55+
4156
observer.observe(document.body, { childList: true, subtree: true });
42-
}
57+
}
4358

44-
function waitForSubmissionResult(slug) {
59+
function waitForSubmissionResult(slug) {
4560
const observer = new MutationObserver(() => {
4661
const resultEl = document.querySelector('span[data-e2e-locator="submission-result"]');
4762
if (resultEl && resultEl.textContent.includes("Accepted")) {
@@ -60,17 +75,16 @@ function getProblemData() {
6075
});
6176

6277
observer.observe(document.body, {childList: true, subtree: true});
63-
}
64-
65-
waitForContentAndStore();
66-
67-
const { slug } = getProblemData();
68-
waitForSubmissionResult(slug);
78+
}
6979

70-
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
80+
// Fix: browser.runtime.onMessage must handle async getProblemData
81+
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
7182
if (message.type === 'GET_PROBLEM_DATA') {
72-
const data = getProblemData();
73-
sendResponse(data);
83+
getProblemData().then((data) => {
84+
sendResponse(data);
85+
});
86+
return true; // Indicate async response
7487
}
75-
});
76-
88+
});
89+
90+
waitForContentAndStore();

leetcodeApi.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
async function fetchLeetCodeTags(slug) {
2+
const query = {
3+
query: `\n query getQuestionDetail($titleSlug: String!) {\n question(titleSlug: $titleSlug) {\n topicTags { name slug }\n }\n }\n `,
4+
variables: { titleSlug: slug },
5+
};
6+
7+
const response = await fetch("https://leetcode.com/graphql", {
8+
method: "POST",
9+
headers: {
10+
"Content-Type": "application/json",
11+
},
12+
body: JSON.stringify(query),
13+
credentials: "same-origin",
14+
});
15+
16+
if (!response.ok) return [];
17+
const data = await response.json();
18+
return data.data?.question?.topicTags?.map((tag) => tag.name) || [];
19+
}

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"content_scripts": [
1717
{
1818
"matches": ["https://leetcode.com/problems/*"],
19-
"js": ["content.js"]
19+
"js": ["leetcodeApi.js", "content.js"]
2020
}
2121
],
2222
"options_ui": {

popup/popup.html

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,19 @@ <h1>KeepCode</h1>
1818
</p>
1919
</div>
2020

21-
<div id="solvedList">
22-
21+
<div>
22+
<h4>Recent Problems</h4>
23+
</div>
24+
<div id="filterBar" style="margin-bottom: 10px; display: flex; align-items: center; gap: 8px;">
25+
<label for="tagFilter" style="font-size: 0.95em;">Filter by tag:</label>
26+
<select id="tagFilter">
27+
<option value="all">All</option>
28+
<!-- Tag options will be populated dynamically -->
29+
</select>
30+
</div>
31+
<div id="solvedList"></div>
32+
<div style="margin-top: 10px; text-align: right;">
33+
<a href="#" id="viewAllLink" style="font-size: 0.95em; color: #2563eb; text-decoration: underline; cursor: pointer;">View All</a>
2334
</div>
2435
<script src="popup.js"></script>
2536
</body>

popup/popup.js

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,25 +41,87 @@ document.addEventListener("DOMContentLoaded", () => {
4141
document.addEventListener("DOMContentLoaded", () => {
4242
browser.storage.local.get(null).then((allData) => {
4343
const problems = Object.values(allData).filter(p => p.status === "Solved");
44-
const list = document.getElementById("solvedList");
44+
// Sort by solvedAt descending (most recent first)
45+
problems.sort((a, b) => (b.solvedAt || 0) - (a.solvedAt || 0));
4546

46-
if (problems.length === 0) {
47-
list.innerHTML = "<p>No solved problems yet</p>"
47+
// Collect all unique tags (flattened)
48+
const tagSet = new Set();
49+
problems.forEach(p => {
50+
if (Array.isArray(p.tags)) {
51+
p.tags.forEach(tag => tagSet.add(tag));
52+
}
53+
});
54+
const tags = Array.from(tagSet);
55+
56+
// Populate tag filter dropdown
57+
const tagFilter = document.getElementById("tagFilter");
58+
if (tagFilter) {
59+
// Remove old options except 'All'
60+
tagFilter.innerHTML = '<option value="all">All</option>';
61+
if (tags.length > 0) {
62+
tags.forEach(tag => {
63+
const opt = document.createElement("option");
64+
opt.value = tag;
65+
opt.textContent = tag;
66+
tagFilter.appendChild(opt);
67+
});
68+
} else {
69+
const opt = document.createElement("option");
70+
opt.value = "none";
71+
opt.textContent = "No tags";
72+
tagFilter.appendChild(opt);
73+
}
4874
}
4975

50-
problems.forEach(problem => {
51-
const item = document.createElement("div");
52-
item.className = "problem-item";
53-
// Add difficulty as a class for color styling
54-
const difficultyClass = problem.difficulty ? problem.difficulty.toLowerCase() : "";
55-
item.innerHTML = `
56-
<a href="${problem.url}" target="_blank">${problem.title}</a>
57-
<span class="difficulty ${difficultyClass}">${problem.difficulty}</span>
58-
`;
59-
list.appendChild(item);
60-
})
61-
})
62-
})
76+
// Render problems (filtered and limited)
77+
function renderProblems() {
78+
const list = document.getElementById("solvedList");
79+
list.innerHTML = "";
80+
let filtered = problems;
81+
const selectedTag = tagFilter ? tagFilter.value : "all";
82+
if (selectedTag && selectedTag !== "all" && selectedTag !== "none") {
83+
filtered = problems.filter(p => Array.isArray(p.tags) && p.tags.includes(selectedTag));
84+
}
85+
const toShow = filtered.slice(0, 5);
86+
if (toShow.length === 0) {
87+
list.innerHTML = "<p>No solved problems yet</p>";
88+
}
89+
toShow.forEach(problem => {
90+
const item = document.createElement("div");
91+
item.className = "problem-item";
92+
const difficultyClass = problem.difficulty ? problem.difficulty.toLowerCase() : "";
93+
// Show tags if available
94+
const tagsHtml = Array.isArray(problem.tags) && problem.tags.length > 0
95+
? `<span style='font-size:0.85em; color:#666; margin-left:8px;'>[${problem.tags.join(", ")}]</span>`
96+
: "<span style='font-size:0.85em; color:#bbb; margin-left:8px;'>[No tags]</span>";
97+
item.innerHTML = `
98+
<a href="${problem.url}" target="_blank">${problem.title}</a>
99+
<span class="difficulty ${difficultyClass}">${problem.difficulty}</span>
100+
${tagsHtml}
101+
`;
102+
list.appendChild(item);
103+
});
104+
}
105+
106+
if (tagFilter) {
107+
tagFilter.addEventListener("change", renderProblems);
108+
}
109+
renderProblems();
110+
});
111+
112+
// View All link opens options page
113+
const viewAllLink = document.getElementById("viewAllLink");
114+
if (viewAllLink) {
115+
viewAllLink.addEventListener("click", (e) => {
116+
e.preventDefault();
117+
if (browser.runtime && browser.runtime.openOptionsPage) {
118+
browser.runtime.openOptionsPage();
119+
} else {
120+
window.open("../options/options.html", "_blank");
121+
}
122+
});
123+
}
124+
});
63125

64126
document.addEventListener("DOMContentLoaded", () => {
65127
const optionsBtn = document.getElementById("optionsBtn");

0 commit comments

Comments
 (0)