Skip to content

Commit 27ae81d

Browse files
authored
Merge pull request #1 from kiing-dom/feat/mvp
Version 0.1.0 mvp now on the addons store
2 parents 6dfce73 + a6af142 commit 27ae81d

File tree

13 files changed

+731
-0
lines changed

13 files changed

+731
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
keepcode.zip
2+
bugs.md

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# leetcode tracker browser extension

background.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// background.js
2+
// Handles persistent messaging and storage for LeetCode Tracker
3+
4+
// Listen for messages from content scripts
5+
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
6+
if (message.type === 'PROBLEM_SOLVED' && message.slug) {
7+
// Update storage with solved status
8+
browser.storage.local.get(message.slug).then((existing) => {
9+
const data = existing[message.slug] || {};
10+
data.status = 'Solved';
11+
data.solvedAt = Date.now();
12+
browser.storage.local.set({ [message.slug]: data });
13+
});
14+
}
15+
// Optionally handle other message types here
16+
});
17+
18+
// Listen for popup requests for problem data
19+
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
20+
if (message.type === 'GET_PROBLEM_DATA' && message.slug) {
21+
browser.storage.local.get(message.slug).then((result) => {
22+
sendResponse(result[message.slug] || null);
23+
});
24+
// Return true to indicate async response
25+
return true;
26+
}
27+
});

content.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Import fetchLeetCodeTags from leetcodeApi.js
2+
// (Assumes leetcodeApi.js is loaded before this script as per manifest)
3+
4+
async function getProblemData() {
5+
const titleEl = document.querySelector('div[class*="text-title-large"]');
6+
const title = titleEl ? titleEl.textContent.trim() : "Unknown Title";
7+
8+
const difficultyEl = Array.from(document.querySelectorAll('div[class*="text-difficulty"]')).find((el) =>
9+
el.textContent?.match(/Easy|Medium|Hard/)
10+
);
11+
const difficulty = difficultyEl
12+
? difficultyEl.textContent.trim()
13+
: "Unknown Difficulty";
14+
15+
const pathParts = window.location.pathname.split("/").filter(Boolean);
16+
const problemSlug = pathParts[1] || "unknown-problem";
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+
26+
// Only return valid data if title and slug are valid
27+
if (!title || title === "Unknown Title" || !problemSlug || problemSlug === "unknown-problem") {
28+
return null;
29+
}
30+
31+
const problemData = {
32+
title,
33+
difficulty,
34+
slug: problemSlug,
35+
url: window.location.href,
36+
timestamp: Date.now(),
37+
tags,
38+
};
39+
40+
console.log("LC Problem Detected:", problemData);
41+
return problemData;
42+
}
43+
44+
// Wait for content and store problem data (with tags)
45+
function waitForContentAndStore() {
46+
const observer = new MutationObserver(async () => {
47+
const titleEl = document.querySelector("div");
48+
if (titleEl && titleEl.textContent.trim()) {
49+
observer.disconnect();
50+
const data = await getProblemData();
51+
if (!data) return; // Don't store invalid/undefined problems
52+
browser.storage.local.set({ [data.slug]: data }).then(() => {
53+
console.log("Saved to storage:", data);
54+
}).catch((err) => {
55+
console.error("Storage error:", err);
56+
});
57+
// Start watching for submission result after we have the slug
58+
waitForSubmissionResult(data.slug);
59+
}
60+
});
61+
62+
observer.observe(document.body, { childList: true, subtree: true });
63+
}
64+
65+
function waitForSubmissionResult(slug) {
66+
const observer = new MutationObserver(() => {
67+
const resultEl = document.querySelector('span[data-e2e-locator="submission-result"]');
68+
if (resultEl && resultEl.textContent.includes("Accepted")) {
69+
console.log("✅ Accepted detected via submission result!");
70+
71+
browser.storage.local.get(slug).then((existing) => {
72+
const data = existing[slug] || {};
73+
data.status = "Solved";
74+
data.solvedAt = Date.now();
75+
browser.storage.local.set({ [slug]: data })
76+
77+
browser.runtime.sendMessage({ type: 'PROBLEM_SOLVED', slug});
78+
observer.disconnect();
79+
});
80+
}
81+
});
82+
83+
observer.observe(document.body, {childList: true, subtree: true});
84+
}
85+
86+
// Fix: browser.runtime.onMessage must handle async getProblemData
87+
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
88+
if (message.type === 'GET_PROBLEM_DATA') {
89+
getProblemData().then((data) => {
90+
sendResponse(data);
91+
});
92+
return true; // Indicate async response
93+
}
94+
});
95+
96+
waitForContentAndStore();

icons/keepcode-icon.png

1.03 MB
Loading

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: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"manifest_version": 2,
3+
"name": "KeepCode",
4+
"version": "0.1.0",
5+
"description": "Focus on answering problems, let KeepCode handle the rest",
6+
"permissions": ["storage", "tabs", "activeTab", "https://leetcode.com/*"],
7+
"background": {
8+
"scripts": ["background.js"],
9+
"persistent": false
10+
},
11+
"browser_action": {
12+
"default_popup": "popup/popup.html",
13+
"default_icon": "icons/keepcode-icon.png"
14+
},
15+
"content_scripts": [
16+
{
17+
"matches": ["https://leetcode.com/problems/*"],
18+
"js": ["leetcodeApi.js", "content.js"]
19+
}
20+
],
21+
"options_ui": {
22+
"page": "options/options.html",
23+
"open_in_tab": true
24+
}
25+
}

options/options.css

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
body {
2+
font-family: Arial, sans-serif;
3+
background: #f8fafc;
4+
margin: 0;
5+
padding: 24px;
6+
min-width: 350px;
7+
}
8+
9+
h1 {
10+
color: #8868bd;
11+
margin-bottom: 18px;
12+
}
13+
14+
section {
15+
background: #f8f8ff;
16+
border-radius: 8px;
17+
padding: 16px;
18+
margin-bottom: 18px;
19+
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
20+
}
21+
22+
a {
23+
color: #2563eb;
24+
text-decoration: none;
25+
}
26+
a:hover {
27+
text-decoration: underline;
28+
}
29+
30+
/* Layout */
31+
.options-container {
32+
display: flex;
33+
min-height: 100vh;
34+
background: #f8fafc;
35+
}
36+
37+
.sidebar {
38+
width: 200px;
39+
background: #fff;
40+
border-right: 1.5px solid #e5e7eb;
41+
padding-top: 32px;
42+
min-height: 100vh;
43+
}
44+
.sidebar ul {
45+
list-style: none;
46+
padding: 0;
47+
margin: 0;
48+
}
49+
.nav-item {
50+
display: flex;
51+
align-items: center;
52+
gap: 12px;
53+
padding: 14px 24px;
54+
cursor: pointer;
55+
color: #374151;
56+
font-size: 1.08em;
57+
border-left: 4px solid transparent;
58+
transition: background 0.15s, border-color 0.15s;
59+
}
60+
.nav-item.active, .nav-item:hover {
61+
background: #f3f4f6;
62+
border-left: 4px solid #8868bd;
63+
color: #8868bd;
64+
}
65+
66+
.main-content {
67+
flex: 1;
68+
padding: 32px 40px;
69+
background: #f8fafc;
70+
}
71+
.section {
72+
display: none;
73+
}
74+
.section.active {
75+
display: block;
76+
}
77+
78+
/* Problems Section */
79+
.problems-toolbar {
80+
display: flex;
81+
gap: 12px;
82+
margin-bottom: 18px;
83+
}
84+
#searchInput {
85+
flex: 1;
86+
padding: 7px 12px;
87+
border: 1px solid #d1d5db;
88+
border-radius: 5px;
89+
font-size: 1em;
90+
}
91+
#tagFilter {
92+
padding: 7px 12px;
93+
border: 1px solid #d1d5db;
94+
border-radius: 5px;
95+
font-size: 1em;
96+
}
97+
#problemsList {
98+
margin-top: 8px;
99+
}
100+
.problem-item {
101+
display: flex;
102+
align-items: center;
103+
justify-content: space-between;
104+
background: #e0e7ef;
105+
border-radius: 6px;
106+
padding: 8px 12px;
107+
margin-bottom: 8px;
108+
}
109+
.problem-item a {
110+
color: #1e293b;
111+
text-decoration: none;
112+
font-weight: 500;
113+
}
114+
.difficulty {
115+
padding: 2px 8px;
116+
border-radius: 4px;
117+
font-size: 0.9em;
118+
font-weight: bold;
119+
}
120+
.difficulty.easy {
121+
background: #d1fae5;
122+
color: #059669;
123+
}
124+
.difficulty.medium {
125+
background: #fef3c7;
126+
color: #b45309;
127+
}
128+
.difficulty.hard {
129+
background: #fee2e2;
130+
color: #b91c1c;
131+
}
132+
133+
/* About/FAQ links */
134+
.section a {
135+
color: #2563eb;
136+
text-decoration: none;
137+
}
138+
.section a:hover {
139+
text-decoration: underline;
140+
}

options/options.html

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>KeepCode Options</title>
6+
<link rel="stylesheet" href="options.css">
7+
<!-- Remix Icon CDN for icons -->
8+
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
9+
</head>
10+
<body>
11+
<div class="options-container">
12+
<nav class="sidebar">
13+
<ul>
14+
<li class="nav-item active" data-section="problems">
15+
<i class="ri-list-check-2 ri-lg"></i>
16+
<span>All Problems</span>
17+
</li>
18+
<li class="nav-item" data-section="settings">
19+
<i class="ri-settings-3-line ri-lg"></i>
20+
<span>Settings</span>
21+
</li>
22+
<li class="nav-item" data-section="about">
23+
<i class="ri-question-line ri-lg"></i>
24+
<span>FAQ / About</span>
25+
</li>
26+
</ul>
27+
</nav>
28+
<main class="main-content">
29+
<section id="section-problems" class="section active">
30+
<h2>All Solved Problems</h2>
31+
<div class="problems-toolbar">
32+
<input type="text" id="searchInput" placeholder="Search by title...">
33+
<select id="tagFilter">
34+
<option value="all">All Tags</option>
35+
</select>
36+
</div>
37+
<div id="problemsList"></div>
38+
</section>
39+
<section id="section-settings" class="section">
40+
<h2>Settings</h2>
41+
<p>Settings will be available here in a future update.</p>
42+
</section>
43+
<section id="section-about" class="section">
44+
<h2>FAQ / About</h2>
45+
<p>
46+
Made by <a href="https://linktr.ee/267dngi" target="_blank">@dngi</a>.<br>
47+
Star the project on <a href="https://github.com/kiing-dom/leetcode-tracker" target="_blank">GitHub</a>!<br>
48+
If you find this useful, consider <a href="https://www.ko-fi.com/267dngi" target="_blank"> buying me a kebab 😏</a>.<br><br>
49+
<strong>FAQ:</strong><br>
50+
<b>Q:</b> How do I use this extension?<br>
51+
<b>A:</b> Just solve problems on LeetCode as usual! Your progress is tracked automatically.<br>
52+
</p>
53+
</section>
54+
</main>
55+
</div>
56+
<script src="options.js"></script>
57+
</body>
58+
</html>

0 commit comments

Comments
 (0)