├── .github
└── FUNDING.yml
├── LICENSE
├── README.md
├── assets
└── logo.jpeg
├── background.js
├── content-script.js
├── images
├── icon-128.png
├── icon-16.png
├── icon-32.png
└── icon-64.png
├── manifest.json
├── popup
├── components.json
├── eslint.config.js
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── prettier.config.cjs
├── public
│ └── logo.png
├── src
│ ├── components
│ │ ├── App.tsx
│ │ ├── Daily.tsx
│ │ ├── EditButton.tsx
│ │ ├── Footer.tsx
│ │ ├── InputForm.tsx
│ │ ├── LeetCode.tsx
│ │ ├── Links.tsx
│ │ ├── Logo.tsx
│ │ ├── Stats.tsx
│ │ ├── Streak.tsx
│ │ ├── Welcome.tsx
│ │ └── ui
│ │ │ ├── Spinner.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── button.tsx
│ │ │ ├── form.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ └── progress.tsx
│ ├── context
│ │ └── userContext.tsx
│ ├── data
│ │ └── links.tsx
│ ├── hooks
│ │ └── leetpush.ts
│ ├── index.css
│ ├── lib
│ │ ├── leetpush.api.ts
│ │ └── utils.ts
│ ├── main.tsx
│ ├── types
│ │ └── leetpush.interface.ts
│ └── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
└── style.css
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | buy_me_a_coffee: husamahmud
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Hüsam
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LeetPush Chrome Extension
2 |
3 | 
4 | 
5 | 
6 | 
7 | 
8 | 
9 | 
10 | 
11 |
12 |
13 |
14 |
15 | ## What is LeetPush?
16 |
17 | LeetPush is a powerful Chrome extension designed for LeetCode enthusiasts who
18 | wish to automate the process of pushing their coding solutions directly to their
19 | GitHub repositories.
20 |
21 | ### Features:
22 |
23 | - One-Click Push: Automatically push your solved problems from LeetCode to
24 | GitHub.
25 | - Easy Configuration: Set up once with your GitHub token and repository details,
26 | and you're all set.
27 | - Seamless Integration: Integrates directly into the LeetCode interface for a
28 | smooth workflow.
29 | - Customization: Choose which solutions to push and manage different
30 | repositories if needed.
31 |
32 | ### Installation and Usage:
33 |
34 | - Download LeetPush from the Chrome Web Store: [**Link**](https://chromewebstore.google.com/detail/leetpush/gmagfdabfjaipjgdfgddjgongeemkalf?hl=en-GB&authuser=0)
35 | - Generate a personal access token on GitHub with repository
36 | access. [**Link**](https://scribehow.com/shared/Generating_a_personal_access_token_on_GitHub__PUPxxuxIRQmlg1MUE-2zig)
37 | - Solve a problem on LeetCode and click on the `Push` button.
38 | - Enter your GitHub username, repository name, and the access token.
39 | - Click on `Push` to push the solution to your GitHub repository.
40 |
41 | ### Explanation Video
42 |
43 | [](https://www.youtube.com/watch?v=7psCr_Pu7GA)
44 |
45 | ### Contributing:
46 |
47 | We welcome contributions! Feel free to open issues or pull requests to help
48 | improve the extension.
49 |
50 | ### License:
51 |
52 | [MIT](LICENSE)
53 |
--------------------------------------------------------------------------------
/assets/logo.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeetPushExtension/LeetPush/1f5082c463092837a34de908c7d5456917604096/assets/logo.jpeg
--------------------------------------------------------------------------------
/background.js:
--------------------------------------------------------------------------------
1 | chrome.tabs.onUpdated.addListener(async (tabId, changeInfo) => {
2 | if (changeInfo.status === 'complete') {
3 | const tab = await chrome.tabs.get(tabId)
4 |
5 | // Only inject on LeetCode pages
6 | if (tab.url?.includes('leetcode.com')) {
7 | chrome.scripting.executeScript({
8 | target: { tabId },
9 | files: ['content-script.js'],
10 | })
11 | }
12 | }
13 | })
14 |
--------------------------------------------------------------------------------
/content-script.js:
--------------------------------------------------------------------------------
1 | ; (() => {
2 | // Constants
3 | const BASE_URL = "https://api.github.com/repos"
4 | const FILE_EXTENSIONS = {
5 | C: ".c",
6 | "C++": ".cpp",
7 | "C#": ".cs",
8 | Dart: ".dart",
9 | Elixir: ".ex",
10 | Erlang: ".erl",
11 | Go: ".go",
12 | Java: ".java",
13 | JavaScript: ".js",
14 | Kotlin: ".kt",
15 | PHP: ".php",
16 | Python: ".py",
17 | Python3: ".py",
18 | Racket: ".rkt",
19 | Ruby: ".rb",
20 | Rust: ".rs",
21 | Scala: ".scala",
22 | Swift: ".swift",
23 | TypeScript: ".ts",
24 | MySQL: ".sql",
25 | PostgreSQL: ".sql",
26 | Oracle: ".sql",
27 | "MS SQL Server": ".tsql",
28 | Pandas: ".py",
29 | }
30 |
31 | const LOCAL_STORAGE_KEYS = {
32 | C: "c",
33 | "C++": "cpp",
34 | "C#": "csharp",
35 | Dart: "dart",
36 | Elixir: "elixir",
37 | Erlang: "erlang",
38 | Go: "golang",
39 | Java: "java",
40 | JavaScript: "javascript",
41 | Kotlin: "kotlin",
42 | PHP: "php",
43 | Python: "python",
44 | Python3: "python3",
45 | Racket: "racket",
46 | Ruby: "ruby",
47 | Rust: "rust",
48 | Scala: "scala",
49 | Swift: "swift",
50 | TypeScript: "typeScript",
51 | MySQL: "mysql",
52 | Oracle: "oraclesql",
53 | PostgreSQL: "postgresql",
54 | "MS SQL Server": "mssql",
55 | Pandas: "pythondata",
56 | }
57 |
58 | const DATABASE_LANGUAGES = ["MySQL", "Oracle", "PostgreSQL", "MS SQL Server", "Pandas"]
59 |
60 | // Platform detection
61 | const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform)
62 |
63 | // Default keyboard shortcuts based on platform
64 | const DEFAULT_SHORTCUT = isMac
65 | ? { key: "p", modifier: "meta" }
66 | : {
67 | key: "p",
68 | modifier: "ctrl",
69 | }
70 |
71 | // Shortcut display text
72 | const getShortcutDisplayText = (shortcut) => {
73 | const modifierSymbol =
74 | shortcut.modifier === "meta"
75 | ? "⌘"
76 | : shortcut.modifier === "alt"
77 | ? "⌥"
78 | : shortcut.modifier === "shift"
79 | ? "⇧"
80 | : shortcut.modifier === "ctrl"
81 | ? "Ctrl+"
82 | : ""
83 | return `${modifierSymbol}${shortcut.key.toUpperCase()}`
84 | }
85 |
86 | // Get keyboard shortcut from localStorage or use default
87 | const getKeyboardShortcut = () => {
88 | const savedShortcut = localStorage.getItem("keyboard-shortcut")
89 | if (savedShortcut) {
90 | try {
91 | return JSON.parse(savedShortcut)
92 | } catch (e) {
93 | console.error("Error parsing saved shortcut:", e)
94 | return DEFAULT_SHORTCUT
95 | }
96 | }
97 | return DEFAULT_SHORTCUT
98 | }
99 |
100 | let KEYBOARD_SHORTCUT = getKeyboardShortcut()
101 | let SHORTCUT_DISPLAY = getShortcutDisplayText(KEYBOARD_SHORTCUT)
102 |
103 | // DOM Selectors
104 | const SELECTORS = {
105 | problemName: "div.flex.items-start.justify-between.gap-4 > div.flex.items-start.gap-2 > div > a",
106 | solutionLanguage:
107 | "div.w-full.flex-1.overflow-y-auto > div > div:nth-child(3) > div.flex.items-center.justify-between.pb-2 > div",
108 | accepted:
109 | "div.text-green-s.dark\\:text-dark-green-s.flex.flex-1.items-center.gap-2.text-\\[16px\\].font-medium.leading-6 > span",
110 | parentDiv:
111 | "div.flex.justify-between.py-1.pl-3.pr-1 > div.relative.flex.overflow-hidden.rounded.bg-fill-tertiary.dark\\:bg-fill-tertiary.\\!bg-transparent > div.flex-none.flex > div:nth-child(2)",
112 | parentDivCodeEditor:
113 | "#ide-top-btns > div:nth-child(1) > div > div > div:nth-child(2) > div > div:nth-child(2) > div > div:last-child",
114 | codeBlock: "div.px-4.py-3 > div > pre > code",
115 | performanceMetrics:
116 | "div.flex.items-center.justify-between.gap-2 > div > div.rounded-sd.flex.min-w-\\[275px\\].flex-1.cursor-pointer.flex-col.px-4.py-3.text-xs > div:nth-child(2) > span.font-semibold",
117 | }
118 |
119 | // Main initialization
120 | document.addEventListener("DOMContentLoaded", initLeetPush)
121 |
122 | function initLeetPush() {
123 | // Only run on submission pages with accepted solutions
124 | if (isSubmissionPage() && hasAcceptedSolution()) {
125 | injectButtons()
126 | extractProblemInfo()
127 | registerKeyboardShortcut()
128 | }
129 | }
130 |
131 | // Helper functions
132 | function isSubmissionPage() {
133 | return window.location.href.includes("submissions")
134 | }
135 |
136 | function hasAcceptedSolution() {
137 | return !!document.querySelector(SELECTORS.accepted)
138 | }
139 |
140 | function sleep(ms) {
141 | return new Promise((resolve) => setTimeout(resolve, ms))
142 | }
143 |
144 | // Register keyboard shortcut
145 | function registerKeyboardShortcut() {
146 | document.addEventListener("keydown", (event) => {
147 | // Check if the shortcut matches the configured one
148 | if (
149 | (KEYBOARD_SHORTCUT.modifier === "meta" && event.metaKey) ||
150 | (KEYBOARD_SHORTCUT.modifier === "alt" && event.altKey) ||
151 | (KEYBOARD_SHORTCUT.modifier === "shift" && event.shiftKey) ||
152 | (KEYBOARD_SHORTCUT.modifier === "ctrl" && event.ctrlKey)
153 | ) {
154 | if (event.key.toLowerCase() === KEYBOARD_SHORTCUT.key.toLowerCase()) {
155 | event.preventDefault()
156 | handlePushClick()
157 | }
158 | }
159 | })
160 | }
161 |
162 | // Button injection
163 | function injectButtons() {
164 | const parentDiv = document.querySelector(SELECTORS.parentDiv)
165 | const parentDivCodeEditor = document.querySelector(SELECTORS.parentDivCodeEditor)
166 |
167 | if (parentDiv) {
168 | injectButtonsToParent(
169 | parentDiv,
170 | "leetpush-div-edit",
171 | "leetpush-btn-edit",
172 | "Edit",
173 | "leetpush-div",
174 | "leetpush-btn",
175 | `Push (${SHORTCUT_DISPLAY})`,
176 | false,
177 | )
178 | }
179 |
180 | if (parentDivCodeEditor) {
181 | injectButtonsToParent(
182 | parentDivCodeEditor,
183 | "leetpush-div-edit-CodeEditor",
184 | "leetpush-btn-edit-CodeEditor",
185 | "Edit",
186 | "leetpush-div-CodeEditor",
187 | "leetpush-btn-CodeEditor",
188 | `Push (${SHORTCUT_DISPLAY})`,
189 | true,
190 | )
191 | }
192 | }
193 |
194 | function injectButtonsToParent(
195 | parent,
196 | editContainerId,
197 | editButtonId,
198 | editText,
199 | pushContainerId,
200 | pushButtonId,
201 | pushText,
202 | isCodeEditor,
203 | ) {
204 | // Don't inject if already present
205 | if (document.getElementById(editContainerId) || document.getElementById(pushContainerId)) {
206 | return
207 | }
208 |
209 | const editButton = createButton(editContainerId, editButtonId, editText, () => {
210 | localStorage.removeItem("branch")
211 | handlePushClick()
212 | })
213 |
214 | const pushButton = createButton(pushContainerId, pushButtonId, pushText, handlePushClick)
215 |
216 | // Check if this is CodeEditor layout
217 | if (isCodeEditor) {
218 | // For CodeEditor layout, ADD dividers and icons
219 | const divider1 = document.createElement("div")
220 | divider1.style.backgroundColor = "#0f0f0f"
221 | divider1.style.width = "1px"
222 | divider1.style.height = "100%"
223 | divider1.style.flexShrink = "0"
224 |
225 | const divider2 = document.createElement("div")
226 | divider2.style.backgroundColor = "#0f0f0f"
227 | divider2.style.width = "1px"
228 | divider2.style.height = "100%"
229 | divider2.style.flexShrink = "0"
230 |
231 | parent.appendChild(divider1)
232 | parent.appendChild(editButton)
233 | parent.appendChild(divider2)
234 | parent.appendChild(pushButton)
235 | } else {
236 | // For toolbar layout, don't add dividers
237 | parent.appendChild(editButton)
238 | parent.appendChild(pushButton)
239 | }
240 | }
241 |
242 | function createButton(containerId, buttonId, text, clickHandler) {
243 | const container = document.createElement("div")
244 | container.id = containerId
245 |
246 | const button = document.createElement("button")
247 | button.id = buttonId
248 | button.textContent = text
249 | button.addEventListener("click", clickHandler)
250 |
251 | container.appendChild(button)
252 | return container
253 | }
254 |
255 | // Problem info extraction
256 | async function extractProblemInfo() {
257 | try {
258 | const probNameElement = document.querySelector(SELECTORS.problemName)
259 | if (!probNameElement) {
260 | console.error("Problem name element not found")
261 | return null
262 | }
263 |
264 | const probNameText = probNameElement.textContent?.trim() || ""
265 | if (!probNameText) {
266 | console.error("Problem name is empty")
267 | return null
268 | }
269 |
270 | const probNum = probNameText.split(".")[0]?.trim() || ""
271 | const probName =
272 | probNameText
273 | .replace(/^\d+\./, "")
274 | .trim()
275 | .replaceAll(" ", "-") || ""
276 |
277 | if (!probNum || !probName) {
278 | console.error("Invalid problem number or name:", { probNum, probName })
279 | return null
280 | }
281 |
282 | // Get solution language
283 | const langElement = document.querySelector(SELECTORS.solutionLanguage).childNodes[2]
284 | if (!langElement) {
285 | console.error("Language element not found")
286 | return null
287 | }
288 |
289 | const solutionLangText = langElement.textContent?.trim() || ""
290 | if (!solutionLangText || !FILE_EXTENSIONS[solutionLangText]) {
291 | console.error("Invalid solution language:", solutionLangText)
292 | return null
293 | }
294 |
295 | const fileExt = FILE_EXTENSIONS[solutionLangText]
296 | const fileName = `${probName}${fileExt}`
297 |
298 | // Get solution from localStorage or DOM
299 | const solutionsId = localStorage.key(0)?.split("_")[1] || ""
300 | let solution = localStorage.getItem(`${probNum}_${solutionsId}_${LOCAL_STORAGE_KEYS[solutionLangText]}`)
301 |
302 | if (!solution) {
303 | const codeElement = document.querySelector(SELECTORS.codeBlock)
304 | solution = codeElement?.textContent || ""
305 | } else {
306 | solution = solution.replace(/\\n/g, "\n").replace(/ {2}/g, " ").replace(/"/g, "")
307 | }
308 |
309 | if (!solution) {
310 | console.error("Solution not found")
311 | return null
312 | }
313 |
314 | // Store in sessionStorage with proper values
315 | sessionStorage.setItem("fileName", fileName)
316 | sessionStorage.setItem("solution", solution)
317 |
318 | // Get performance metrics and create commit message
319 | let commitMsg = ""
320 | if (DATABASE_LANGUAGES.includes(solutionLangText)) {
321 | const metrics = document.querySelectorAll(SELECTORS.performanceMetrics)
322 | const queryRuntimeText = metrics[1]?.textContent || "N/A"
323 | commitMsg = `[${probNum}] [Time Beats: ${queryRuntimeText}] - LeetPush`
324 | } else {
325 | const metrics = document.querySelectorAll(SELECTORS.performanceMetrics)
326 | const runtimeText = metrics[1]?.textContent || "N/A"
327 | const memoryText = metrics[3]?.textContent || "N/A"
328 | commitMsg = `[${probNum}] [Time Beats: ${runtimeText}] [Memory Beats: ${memoryText}] - LeetPush`
329 | }
330 |
331 | sessionStorage.setItem("commitMsg", commitMsg)
332 |
333 | // Log successful extraction
334 | console.log("Problem info extracted successfully:", {
335 | probNum,
336 | probName,
337 | fileName,
338 | solution: solution.substring(0, 50) + "...",
339 | commitMsg,
340 | language: solutionLangText,
341 | })
342 |
343 | return {
344 | probNum,
345 | probName,
346 | fileName,
347 | solution,
348 | commitMsg,
349 | language: solutionLangText,
350 | }
351 | } catch (error) {
352 | console.error("Error extracting problem info:", error)
353 | return null
354 | }
355 | }
356 |
357 | // Modal creation and handling
358 | function createConfigModal() {
359 | const modal = document.createElement("div")
360 | modal.id = "lp-modal"
361 | modal.innerHTML = `
362 |
363 |
364 |
LeetPush
365 |
419 |
420 | `
421 |
422 | // Add event listeners
423 | modal.querySelector("#lp-close-btn button")?.addEventListener("click", () => {
424 | document.body.removeChild(modal)
425 | })
426 |
427 | // Close modal when clicking outside the modal container
428 | modal.addEventListener("click", (event) => {
429 | if (event.target === modal) {
430 | document.body.removeChild(modal)
431 | }
432 | })
433 |
434 | document.addEventListener("keydown", (event) => {
435 | if (event.key === "Escape" && document.body.contains(modal)) {
436 | document.body.removeChild(modal)
437 | }
438 | })
439 |
440 | modal.querySelector("#lp-form")?.addEventListener("submit", async (event) => {
441 | event.preventDefault()
442 | await saveConfig(modal)
443 | })
444 |
445 | return modal
446 | }
447 |
448 | async function saveConfig(modal) {
449 | const repoUrlInput = modal.querySelector("#repo-url")
450 | const tokenInput = modal.querySelector("#token")
451 | const branchInput = modal.querySelector('input[name="branch-name"]:checked')
452 | const separateFolderInput = modal.querySelector('input[name="daily-challenge"]:checked')
453 | const customDirInput = modal.querySelector("#custom-dir")
454 | const shortcutModifierInput = modal.querySelector("#shortcut-modifier")
455 | const shortcutKeyInput = modal.querySelector("#shortcut-key")
456 |
457 | if (!repoUrlInput || !tokenInput || !branchInput || !separateFolderInput) return
458 |
459 | // Validate GitHub URL format
460 | const githubUrlPattern = /^https:\/\/github\.com\/[\w-]+\/[\w.-]+$/
461 | if (!githubUrlPattern.test(repoUrlInput.value)) {
462 | alert("Please enter a valid GitHub repository URL (https://github.com/username/repository).")
463 | return
464 | }
465 |
466 | // Validate GitHub token (basic format check)
467 | if (!tokenInput.value.startsWith("ghp_") && !tokenInput.value.startsWith("github_pat_")) {
468 | alert('Please enter a valid GitHub token. It should start with "ghp_" or "github_pat_".')
469 | return
470 | }
471 |
472 | const repoUrl = repoUrlInput.value.endsWith(".git") ? repoUrlInput.value.slice(0, -4) : repoUrlInput.value
473 | const token = tokenInput.value
474 | const branch = branchInput.value
475 | const separateFolder = separateFolderInput.value
476 | const customDir = customDirInput?.value || ""
477 |
478 | // Save shortcut if provided
479 | if (shortcutModifierInput && shortcutKeyInput && shortcutKeyInput.value) {
480 | const shortcutKey = shortcutKeyInput.value.toLowerCase()
481 | const shortcutModifier = shortcutModifierInput.value
482 |
483 | KEYBOARD_SHORTCUT = { key: shortcutKey, modifier: shortcutModifier }
484 | SHORTCUT_DISPLAY = getShortcutDisplayText(KEYBOARD_SHORTCUT)
485 |
486 | localStorage.setItem("keyboard-shortcut", JSON.stringify(KEYBOARD_SHORTCUT))
487 |
488 | // Update button labels with new shortcut
489 | updateButtonLabels()
490 | }
491 |
492 | // Save to localStorage
493 | localStorage.setItem("repo", repoUrl)
494 | localStorage.setItem("token", token)
495 | localStorage.setItem("branch", branch)
496 | localStorage.setItem("separate-folder", separateFolder)
497 | localStorage.setItem("custom-dir", customDir)
498 |
499 | document.body.removeChild(modal)
500 |
501 | // Update README and description
502 | try {
503 | await updateRepoDescription(token, repoUrl, branch)
504 | } catch (error) {
505 | console.error("Error setting up repository metadata:", error)
506 | showError("Initial repository setup failed. You may need to check your repository permissions.")
507 | }
508 | }
509 |
510 | function updateButtonLabels() {
511 | const buttons = [document.querySelector("#leetpush-btn"), document.querySelector("#leetpush-btn-CodeEditor")]
512 |
513 | buttons.forEach((button) => {
514 | if (button) {
515 | button.textContent = `Push (${SHORTCUT_DISPLAY})`
516 | }
517 | })
518 | }
519 |
520 | function showConfigModal() {
521 | const modal = createConfigModal()
522 |
523 | // Pre-fill with existing values if available
524 | const token = localStorage.getItem("token")
525 | const repo = localStorage.getItem("repo")
526 | const branch = localStorage.getItem("branch")
527 | const separateFolder = localStorage.getItem("separate-folder")
528 | const customDir = localStorage.getItem("custom-dir")
529 |
530 | // Pre-fill shortcut values
531 | const shortcutModifierInput = modal.querySelector("#shortcut-modifier")
532 | const shortcutKeyInput = modal.querySelector("#shortcut-key")
533 |
534 | if (shortcutModifierInput) {
535 | shortcutModifierInput.value = KEYBOARD_SHORTCUT.modifier
536 | }
537 |
538 | if (shortcutKeyInput) {
539 | shortcutKeyInput.value = KEYBOARD_SHORTCUT.key.toUpperCase()
540 | }
541 |
542 | if (token) modal.querySelector("#token").value = token
543 | if (repo) modal.querySelector("#repo-url").value = repo
544 | if (branch) modal.querySelector(`#branch-${branch}`).checked = true
545 | if (separateFolder) modal.querySelector(`#separate-folder-${separateFolder}`).checked = true
546 | if (customDir) modal.querySelector("#custom-dir").value = customDir
547 |
548 | document.body.appendChild(modal)
549 | }
550 |
551 | // Function to show error messages to the user
552 | function showError(message) {
553 | alert(`LeetPush Error: ${message}`)
554 | }
555 |
556 | // GitHub API interactions
557 | async function handlePushClick() {
558 | const config = getGithubConfig()
559 |
560 | if (!isConfigComplete(config)) {
561 | showConfigModal()
562 | return
563 | }
564 |
565 | const pushBtn = getPushButton()
566 | if (!pushBtn) {
567 | console.error("Push button not found")
568 | showError("Interface error: Push button not found. Please refresh the page and try again.")
569 | return
570 | }
571 |
572 | // Force a refresh of problem info
573 | const problemInfo = await extractProblemInfo()
574 | if (!problemInfo) {
575 | console.error("Failed to extract problem info")
576 | showError(
577 | "Failed to extract problem information. This may happen if the page structure has changed or if you're not on a solution page.",
578 | )
579 | return
580 | }
581 |
582 | const fileName = problemInfo.fileName
583 | const solution = problemInfo.solution
584 | const commitMsg = problemInfo.commitMsg
585 |
586 | if (!fileName || !solution || !commitMsg) {
587 | console.error("Missing required data:", {
588 | fileName,
589 | hasSolution: !!solution,
590 | hasCommitMsg: !!commitMsg,
591 | })
592 |
593 | if (!fileName) {
594 | showError("Failed to generate a valid file name for your solution.")
595 | } else if (!solution) {
596 | showError("Failed to extract your solution code. Please try again.")
597 | } else {
598 | showError("Failed to generate commit message. Please try again.")
599 | }
600 | return
601 | }
602 |
603 | const [userName, repoName] = config.repo.split("/").slice(3, 5)
604 | if (!userName || !repoName) {
605 | console.error("Invalid repository URL:", config.repo)
606 | showError(
607 | "Invalid repository URL format. Please check your settings and provide a valid GitHub repository URL (e.g., https://github.com/username/repository).",
608 | )
609 | showConfigModal()
610 | return
611 | }
612 |
613 | pushBtn.disabled = true
614 | pushBtn.textContent = "Loading..."
615 | pushBtn.classList.add("loading")
616 |
617 | try {
618 | const result = await pushToGithub(
619 | userName,
620 | repoName,
621 | config.branch,
622 | fileName,
623 | solution,
624 | commitMsg,
625 | config.token,
626 | config.separateFolder,
627 | config.customDir,
628 | )
629 |
630 | pushBtn.classList.remove("loading")
631 | pushBtn.classList.add("success")
632 | pushBtn.textContent = "Done"
633 | await sleep(2000)
634 | pushBtn.disabled = false
635 | pushBtn.classList.remove("success")
636 | pushBtn.textContent = `Push (${SHORTCUT_DISPLAY})`
637 |
638 | // Update statistics
639 | const solutionsPushed = Number.parseInt(localStorage.getItem("solutions-pushed") || "0") + 1
640 | localStorage.setItem("solutions-pushed", solutionsPushed.toString())
641 |
642 | // Check if it's a daily challenge
643 | try {
644 | const [, dailyProblemNum] = await getDailyChallenge()
645 |
646 | if (problemInfo && dailyProblemNum === problemInfo.probNum) {
647 | const dailyChallenges = Number.parseInt(localStorage.getItem("daily-challenges") || "0") + 1
648 | localStorage.setItem("daily-challenges", dailyChallenges.toString())
649 | }
650 | } catch (error) {
651 | console.error("Error checking daily challenge:", error)
652 | // Non-critical error, don't show to user
653 | }
654 | } catch (error) {
655 | console.error("Failed to push solution:", error)
656 | showError(error.message || "Unknown error occurred while pushing solution")
657 |
658 | pushBtn.classList.remove("loading")
659 | pushBtn.classList.add("error")
660 | pushBtn.textContent = "Error"
661 | await sleep(2000)
662 | pushBtn.disabled = false
663 | pushBtn.classList.remove("error")
664 | pushBtn.textContent = `Push (${SHORTCUT_DISPLAY})`
665 | }
666 | }
667 |
668 | function getGithubConfig() {
669 | return {
670 | token: localStorage.getItem("token") || "",
671 | repo: localStorage.getItem("repo") || "",
672 | branch: localStorage.getItem("branch") || "",
673 | separateFolder: localStorage.getItem("separate-folder") || "",
674 | customDir: localStorage.getItem("custom-dir") || "",
675 | }
676 | }
677 |
678 | function isConfigComplete(config) {
679 | return !!(config.token && config.repo && config.branch && config.separateFolder !== null)
680 | }
681 |
682 | function getPushButton() {
683 | return document.querySelector("#leetpush-btn") || document.querySelector("#leetpush-btn-CodeEditor")
684 | }
685 |
686 | async function pushToGithub(
687 | userName,
688 | repoName,
689 | branch,
690 | fileName,
691 | content,
692 | commitMsg,
693 | token,
694 | separateFolder,
695 | customDir,
696 | ) {
697 | try {
698 | // Check if fileName is valid
699 | if (!fileName || fileName.trim() === "") {
700 | console.error("Invalid file name:", fileName)
701 | throw new Error("Invalid file name. Please try again with a different solution.")
702 | }
703 |
704 | // Check if content is valid
705 | if (!content || content.trim() === "") {
706 | console.error("Invalid content:", content)
707 | throw new Error("No solution content found. Please make sure your solution is visible on the page.")
708 | }
709 |
710 | // Debug logs to track values
711 | console.log("Debug - fileName:", fileName)
712 | console.log("Debug - commitMsg:", commitMsg)
713 | console.log("Debug - content length:", content.length)
714 |
715 | // Determine file path
716 | let filePath = fileName
717 |
718 | if (customDir) {
719 | filePath = `${customDir}/${fileName}`
720 | } else if (separateFolder === "yes") {
721 | try {
722 | const [date, dailyProblemNum] = await getDailyChallenge()
723 | const problemInfo = await extractProblemInfo()
724 |
725 | if (problemInfo && dailyProblemNum === problemInfo.probNum) {
726 | const splitDate = date.split("-")
727 | const dailyFolder = `DCP-${splitDate[1]}-${splitDate[0].slice(2)}`
728 | filePath = `${dailyFolder}/${fileName}`
729 | }
730 | } catch (error) {
731 | console.error("Error processing daily challenge folder:", error)
732 | // Continue without separate folder if there's an error
733 | }
734 | }
735 |
736 | // Ensure filePath is not empty
737 | if (!filePath || filePath.trim() === "") {
738 | console.error("Invalid file path:", filePath)
739 | throw new Error("Failed to generate a valid file path. Please check your target directory settings.")
740 | }
741 |
742 | console.log("Debug - final filePath:", filePath)
743 |
744 | // Push file to repo
745 | return await pushFileToRepo(userName, repoName, filePath, branch, content, commitMsg, token)
746 | } catch (error) {
747 | console.error("Error in pushToGithub:", error)
748 | throw error // Propagate the error to be handled by the caller
749 | }
750 | }
751 |
752 | async function pushFileToRepo(userName, repoName, filePath, branch, content, commitMsg, token) {
753 | if (!filePath || !content || !commitMsg) {
754 | console.error("Missing required parameters:", {
755 | filePath,
756 | hasContent: !!content,
757 | hasCommitMsg: !!commitMsg,
758 | })
759 | throw new Error("Missing required file information. Please try again.")
760 | }
761 |
762 | const apiUrl = `${BASE_URL}/${userName}/${repoName}/contents/${filePath}`
763 | console.log("API URL:", apiUrl)
764 |
765 | try {
766 | // Check if repo exists
767 | const repoCheckResponse = await fetch(`${BASE_URL}/${userName}/${repoName}`, {
768 | headers: {
769 | Authorization: `Bearer ${token}`,
770 | },
771 | })
772 | console.log("Repo check response:", repoCheckResponse)
773 |
774 | if (!repoCheckResponse.ok) {
775 | const errorData = await repoCheckResponse.json()
776 | console.error("Repository check error:", errorData)
777 |
778 | // Handle different error cases with specific messages
779 | if (repoCheckResponse.status === 404) {
780 | throw new Error(
781 | `Repository not found: ${userName}/${repoName}. Please check if the repository exists and your token has access to it.`,
782 | )
783 | } else if (repoCheckResponse.status === 401) {
784 | throw new Error(
785 | "Authentication failed. Your GitHub token may be invalid or expired. Please generate a new token.",
786 | )
787 | } else if (repoCheckResponse.status === 403) {
788 | throw new Error("Access forbidden. Your token may not have sufficient permissions to access this repository.")
789 | } else {
790 | throw new Error(`Repository access error: ${errorData.message || "Unknown error"}`)
791 | }
792 | }
793 |
794 | // Prepare request body with proper content encoding
795 | const encodedContent = btoa(unescape(encodeURIComponent(content)))
796 | const requestBody = {
797 | message: commitMsg,
798 | content: encodedContent,
799 | branch: branch,
800 | }
801 |
802 | // Check if file exists and get the latest SHA
803 | let fileExistsRes
804 | try {
805 | fileExistsRes = await fetch(`${apiUrl}?ref=${branch}`, {
806 | headers: {
807 | Authorization: `Bearer ${token}`,
808 | },
809 | })
810 | console.log("File exists response:", fileExistsRes)
811 | } catch (error) {
812 | console.error("Error checking if file exists:", error)
813 | throw new Error("Network error while checking if file already exists in repository.")
814 | }
815 |
816 | if (fileExistsRes.ok) {
817 | try {
818 | const existingFileData = await fileExistsRes.json()
819 | if (existingFileData && existingFileData.sha) {
820 | requestBody.sha = existingFileData.sha
821 | console.log("Found existing file SHA:", existingFileData.sha)
822 | }
823 | } catch (error) {
824 | console.error("Error parsing existing file data:", error)
825 | throw new Error("Error processing existing file data from GitHub.")
826 | }
827 | } else if (fileExistsRes.status !== 404) {
828 | // 404 is expected if file doesn't exist yet
829 | const errorData = await fileExistsRes.json()
830 | console.error("Error checking file existence:", errorData)
831 |
832 | if (fileExistsRes.status === 403) {
833 | throw new Error(
834 | 'Permission denied. Make sure your token has "contents: write" permission on this repository.',
835 | )
836 | } else if (fileExistsRes.status === 401) {
837 | throw new Error("Authentication failed. Your GitHub token may be invalid or expired.")
838 | } else {
839 | throw new Error(`Error checking file: ${errorData.message || "Unknown error"}`)
840 | }
841 | }
842 |
843 | // Make the API call to push the file
844 | let response = await fetch(apiUrl, {
845 | method: "PUT",
846 | headers: {
847 | "Content-Type": "application/json",
848 | Authorization: `Bearer ${token}`,
849 | },
850 | body: JSON.stringify(requestBody),
851 | })
852 | console.log("Response status:", response.status)
853 |
854 | // Handle SHA mismatch (409 conflict) error - retry up to 3 times
855 | let retryCount = 0
856 | const maxRetries = 3
857 |
858 | while (response.status === 409 && retryCount < maxRetries) {
859 | console.log(
860 | `SHA mismatch detected (attempt ${retryCount + 1}/${maxRetries}), fetching updated SHA and retrying...`,
861 | )
862 | retryCount++
863 |
864 | try {
865 | // Get the latest SHA again
866 | const latestShaRes = await fetch(`${apiUrl}?ref=${branch}`, {
867 | headers: {
868 | Authorization: `Bearer ${token}`,
869 | },
870 | })
871 |
872 | if (!latestShaRes.ok) {
873 | console.error(`Failed to get latest SHA on retry ${retryCount}, status: ${latestShaRes.status}`)
874 | continue // Skip to next retry attempt
875 | }
876 |
877 | const latestFileData = await latestShaRes.json()
878 | if (latestFileData && latestFileData.sha) {
879 | // Update the SHA in the request body
880 | requestBody.sha = latestFileData.sha
881 | console.log(`Using updated SHA on retry ${retryCount}: ${latestFileData.sha}`)
882 |
883 | // Retry the request with the updated SHA
884 | response = await fetch(apiUrl, {
885 | method: "PUT",
886 | headers: {
887 | "Content-Type": "application/json",
888 | Authorization: `Bearer ${token}`,
889 | },
890 | body: JSON.stringify(requestBody),
891 | })
892 | console.log(`Retry ${retryCount} response status:`, response.status)
893 |
894 | if (response.ok) {
895 | break // Success, exit the retry loop
896 | }
897 | } else {
898 | console.error("Failed to get valid SHA from response:", latestFileData)
899 | break
900 | }
901 | } catch (retryError) {
902 | console.error(`Error during retry ${retryCount}:`, retryError)
903 | break // Exit retry loop on error
904 | }
905 |
906 | // Small delay between retries to avoid rate limiting
907 | await sleep(500)
908 | }
909 |
910 | if (!response.ok) {
911 | let errorMessage = `GitHub API Error: ${response.status}`
912 | try {
913 | const errorData = await response.json()
914 | errorMessage += ` - ${errorData.message || "Unknown error"}`
915 | console.error("GitHub API error details:", errorData)
916 | } catch (jsonError) {
917 | console.error("Failed to parse error response:", jsonError)
918 | }
919 | throw new Error(errorMessage)
920 | }
921 |
922 | const responseData = await response.json()
923 | console.log("File successfully pushed:", responseData.content.html_url)
924 | return true
925 | } catch (error) {
926 | console.error("Error pushing file to repo:", error)
927 | // Don't show alert here, just propagate the error to be handled by the caller
928 | throw error
929 | }
930 | }
931 |
932 | async function updateRepoDescription(token, repo, branch) {
933 | const [userName, repoName] = repo.split("/").slice(3, 5)
934 | const description = "This repository is managed by LeetPush extension: https://github.com/husamahmud/LeetPush"
935 |
936 | try {
937 | await fetch(`${BASE_URL}/${userName}/${repoName}`, {
938 | method: "PATCH",
939 | headers: {
940 | "Content-Type": "application/json",
941 | Authorization: `Bearer ${token}`,
942 | },
943 | body: JSON.stringify({ description }),
944 | })
945 | } catch (error) {
946 | console.error("Error updating repo metadata:", error)
947 | }
948 | }
949 |
950 | async function getDailyChallenge() {
951 | const response = await fetch("https://leetcode.com/graphql", {
952 | method: "POST",
953 | headers: {
954 | "Content-Type": "application/json",
955 | },
956 | body: JSON.stringify({
957 | query: `{
958 | activeDailyCodingChallengeQuestion {
959 | date
960 | question {
961 | frontendQuestionId: questionFrontendId
962 | }
963 | }
964 | }`,
965 | }),
966 | })
967 |
968 | const data = await response.json()
969 | return [
970 | data.data.activeDailyCodingChallengeQuestion.date,
971 | data.data.activeDailyCodingChallengeQuestion.question.frontendQuestionId,
972 | ]
973 | }
974 |
975 | // Initialize on page load
976 | initLeetPush()
977 |
978 | // Re-initialize on DOM changes (for single-page apps)
979 | const observer = new MutationObserver((mutations) => {
980 | if (isSubmissionPage() && hasAcceptedSolution()) {
981 | const hasButtons = document.getElementById("leetpush-btn") || document.getElementById("leetpush-btn-CodeEditor")
982 | if (!hasButtons) {
983 | initLeetPush()
984 | }
985 | }
986 | })
987 |
988 | observer.observe(document.body, { childList: true, subtree: true })
989 | })()
990 |
--------------------------------------------------------------------------------
/images/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeetPushExtension/LeetPush/1f5082c463092837a34de908c7d5456917604096/images/icon-128.png
--------------------------------------------------------------------------------
/images/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeetPushExtension/LeetPush/1f5082c463092837a34de908c7d5456917604096/images/icon-16.png
--------------------------------------------------------------------------------
/images/icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeetPushExtension/LeetPush/1f5082c463092837a34de908c7d5456917604096/images/icon-32.png
--------------------------------------------------------------------------------
/images/icon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeetPushExtension/LeetPush/1f5082c463092837a34de908c7d5456917604096/images/icon-64.png
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "LeetPush",
4 | "version": "1.7.4",
5 | "description": "Effortlessly capture and push LeetCode solutions to your GitHub repository",
6 | "background": {
7 | "service_worker": "background.js",
8 | "type": "module"
9 | },
10 | "action": {
11 | "default_popup": "dist/index.html",
12 | "default_icon": {
13 | "16": "images/icon-16.png",
14 | "32": "images/icon-32.png",
15 | "64": "images/icon-64.png",
16 | "128": "images/icon-128.png"
17 | }
18 | },
19 | "content_scripts": [
20 | {
21 | "matches": [
22 | "https://*.leetcode.com/*"
23 | ],
24 | "js": [
25 | "content-script.js"
26 | ],
27 | "css": [
28 | "style.css"
29 | ]
30 | }
31 | ],
32 | "permissions": [
33 | "scripting",
34 | "storage"
35 | ],
36 | "host_permissions": [
37 | "https://*.leetcode.com/*",
38 | "https://api.github.com/*"
39 | ],
40 | "icons": {
41 | "16": "images/icon-16.png",
42 | "32": "images/icon-32.png",
43 | "64": "images/icon-64.png",
44 | "128": "images/icon-128.png"
45 | }
46 | }
--------------------------------------------------------------------------------
/popup/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/index.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/popup/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config({
8 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
9 | files: ['**/*.{ts,tsx}'],
10 | ignores: ['dist'],
11 | languageOptions: {
12 | ecmaVersion: 2020,
13 | globals: globals.browser,
14 | },
15 | plugins: {
16 | 'react-hooks': reactHooks,
17 | 'react-refresh': reactRefresh,
18 | },
19 | rules: {
20 | ...reactHooks.configs.recommended.rules,
21 | 'react-refresh/only-export-components': [
22 | 'warn',
23 | { allowConstantExport: true },
24 | ],
25 | },
26 | })
27 |
--------------------------------------------------------------------------------
/popup/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
10 | LeetPush
11 |
12 |
13 |
14 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/popup/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "popup",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@hookform/resolvers": "^3.9.0",
14 | "@radix-ui/react-label": "^2.1.0",
15 | "@radix-ui/react-progress": "^1.1.0",
16 | "@radix-ui/react-slot": "^1.1.0",
17 | "@radix-ui/react-tooltip": "^1.1.2",
18 | "@tanstack/react-query": "^5.51.23",
19 | "@tanstack/react-query-devtools": "^5.51.23",
20 | "antd": "^5.20.1",
21 | "axios": "^1.7.3",
22 | "class-variance-authority": "^0.7.0",
23 | "clsx": "^2.1.1",
24 | "lucide-react": "^0.427.0",
25 | "react": "^18.3.1",
26 | "react-dom": "^18.3.1",
27 | "react-hook-form": "^7.52.2",
28 | "react-icons": "^5.3.0",
29 | "tailwind-merge": "^2.4.0",
30 | "tailwindcss-animate": "^1.0.7",
31 | "zod": "^3.23.8"
32 | },
33 | "devDependencies": {
34 | "@eslint/js": "^9.8.0",
35 | "@types/node": "^22.2.0",
36 | "@types/react": "^18.3.3",
37 | "@types/react-dom": "^18.3.0",
38 | "@vitejs/plugin-react-swc": "^3.5.0",
39 | "autoprefixer": "^10.4.20",
40 | "eslint": "^9.8.0",
41 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
42 | "eslint-plugin-react-refresh": "^0.4.9",
43 | "globals": "^15.9.0",
44 | "postcss": "^8.4.41",
45 | "prettier": "^3.3.3",
46 | "prettier-plugin-tailwindcss": "^0.6.6",
47 | "tailwindcss": "^3.4.9",
48 | "typescript": "^5.5.3",
49 | "typescript-eslint": "^8.0.0",
50 | "vite": "^5.4.0"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/popup/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/popup/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: ['prettier-plugin-tailwindcss'],
3 | tailwindConfig: './tailwind.config.js',
4 | singleQuote: true,
5 | semi: false,
6 | trailingComma: 'all',
7 | tabWidth: 2,
8 | printWidth: 85,
9 | jsxBracketSameLine: true,
10 | }
11 |
--------------------------------------------------------------------------------
/popup/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeetPushExtension/LeetPush/1f5082c463092837a34de908c7d5456917604096/popup/public/logo.png
--------------------------------------------------------------------------------
/popup/src/components/App.tsx:
--------------------------------------------------------------------------------
1 | import Links from '@/components/Links.tsx'
2 | import Logo from '@/components/Logo.tsx'
3 | import LeetCode from '@/components/LeetCode.tsx'
4 | import Footer from '@/components/Footer.tsx'
5 | import InputForm from '@/components/InputForm.tsx'
6 | import { useContext } from 'react'
7 | import { UserContext } from '@/context/userContext.tsx'
8 | import EditButton from '@/components/EditButton.tsx'
9 |
10 | export default function App() {
11 | const { username } = useContext(UserContext)
12 |
13 | return (
14 |
15 |
16 |
17 | {username ? (
18 |
19 | ) : (
20 |
21 | )}
22 | {username && }
23 |
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/popup/src/components/Daily.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from '@/components/ui/badge'
2 |
3 | import { getDifficultyColor } from '@/lib/utils.ts'
4 | import { DailyProblemI } from '@/types/leetpush.interface.ts'
5 |
6 | export default function Daily({ data }: { data: DailyProblemI }) {
7 | return (
8 |
9 |
10 |
11 |
29 |
30 |
31 | {data.question.topicTags.map((tag) => (
32 |
34 | {tag.name}
35 |
36 | ))}
37 |
38 |
39 |
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/popup/src/components/EditButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button.tsx'
2 | import { UserContext } from '@/context/userContext.tsx'
3 | import { useContext } from 'react'
4 |
5 | export default function EditButton() {
6 | const { setUsername } = useContext(UserContext)
7 |
8 | return (
9 |
10 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/popup/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { FaXTwitter, FaLinkedinIn } from 'react-icons/fa6'
2 |
3 | export default function Footer() {
4 | return (
5 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/popup/src/components/InputForm.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, FormEvent, useState } from 'react'
2 |
3 | import { Button } from '@/components/ui/button'
4 | import { Input } from '@/components/ui/input'
5 |
6 | import { UserContext } from '@/context/userContext.tsx'
7 | import { fetchUserStats } from '@/lib/leetpush.api.ts'
8 |
9 | export default function InputForm() {
10 | const { username, setUsername } = useContext(UserContext)
11 | const [loading, setLoading] = useState(false)
12 | const [error, setError] = useState('')
13 |
14 | const handleSubmit = async (e: FormEvent) => {
15 | e.preventDefault()
16 | setLoading(true)
17 |
18 | const username = e.currentTarget.username.value
19 | if (!username) {
20 | setError('Please enter a username')
21 | setLoading(false)
22 | return
23 | }
24 |
25 | try {
26 | await fetchUserStats(username)
27 |
28 | setUsername(username)
29 | setError('')
30 | } catch (error) {
31 | if (error instanceof Error) {
32 | if (error.message === 'User not found') {
33 | setError('User not found')
34 | } else {
35 | setError('Failed to fetch user stats')
36 | }
37 | } else {
38 | setError('An unknown error occurred')
39 | }
40 | } finally {
41 | setLoading(false)
42 | }
43 | }
44 |
45 | return (
46 |
47 |
59 |
60 | {error &&
{error}
}
61 |
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/popup/src/components/LeetCode.tsx:
--------------------------------------------------------------------------------
1 | import Welcome from '@/components/Welcome.tsx'
2 | import Daily from '@/components/Daily.tsx'
3 | import Spinner from '@/components/ui/Spinner.tsx'
4 | import Stats from '@/components/Stats.tsx'
5 | import Streak from '@/components/Streak.tsx'
6 |
7 | import {
8 | useDailyProblem,
9 | useUserStats,
10 | useUserStreak,
11 | } from '@/hooks/leetpush.ts'
12 | import {
13 | DailyProblemI,
14 | UserStatsI,
15 | UserStreakI,
16 | } from '@/types/leetpush.interface.ts'
17 | import { useContext } from 'react'
18 | import { UserContext } from '@/context/userContext.tsx'
19 |
20 | export default function LeetCode() {
21 | const { username } = useContext(UserContext)
22 |
23 | const {
24 | data: dailyProblemData,
25 | error: dailyProblemError,
26 | isLoading: isDailyProblemLoading,
27 | } = useDailyProblem()
28 |
29 | const {
30 | data: userStatsData,
31 | error: userStatsError,
32 | isLoading: isUserStatsLoading,
33 | } = useUserStats(username)
34 |
35 | const {
36 | data: userStreakData,
37 | error: userStreakError,
38 | isLoading: isUserStreakLoadin,
39 | } = useUserStreak(username)
40 |
41 | const isLoading = isDailyProblemLoading || isUserStatsLoading || isUserStreakLoadin
42 | const error = dailyProblemError || userStatsError || userStreakError
43 | const totalProblems = userStatsData?.acSubmissionNum[0]?.count
44 |
45 | return (
46 |
47 | {isLoading
48 | ?
49 | : error ? (
50 |
51 |
Error: {error.message}
52 |
53 | ) : (
54 |
55 |
57 |
58 |
59 |
60 |
61 | )}
62 |
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/popup/src/components/Links.tsx:
--------------------------------------------------------------------------------
1 | import { LINKS } from '@/data/links.tsx'
2 |
3 | export default function Links() {
4 | return (
5 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/popup/src/components/Logo.tsx:
--------------------------------------------------------------------------------
1 | export default function Logo() {
2 | return (
3 |
4 |

5 |
6 | LeetPush
7 |
8 |
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/popup/src/components/Stats.tsx:
--------------------------------------------------------------------------------
1 | import { UserStatsI } from '@/types/leetpush.interface.ts'
2 |
3 | export default function Stats({ data }: { data: UserStatsI }) {
4 | const { allQuestionsCount, acSubmissionNum } = data
5 |
6 | const allQuestions = {
7 | all: allQuestionsCount[0].count,
8 | easy: allQuestionsCount[1].count,
9 | medium: allQuestionsCount[2].count,
10 | hard: allQuestionsCount[3].count,
11 | }
12 |
13 | const acSubmissions = {
14 | all: acSubmissionNum[0].count,
15 | easy: acSubmissionNum[1].count,
16 | medium: acSubmissionNum[2].count,
17 | hard: acSubmissionNum[3].count,
18 | }
19 |
20 | const easyPercentage: number = (acSubmissions.easy / allQuestions.easy) * 100
21 | const mediumPercentage: number = (acSubmissions.medium / allQuestions.medium) * 100
22 | const hardPercentage: number = (acSubmissions.hard / allQuestions.hard) * 100
23 |
24 | return (
25 |
26 |
27 |
28 | Easy
29 |
30 | {acSubmissions.easy}/{allQuestions.easy}
31 |
32 |
33 |
34 |
35 | Medium
36 |
37 | {acSubmissions.medium}/{allQuestions.medium}
38 |
39 |
40 |
41 |
42 | Hard
43 |
44 | {acSubmissions.hard}/{allQuestions.hard}
45 |
46 |
47 |
48 |
49 |
50 |
54 |
55 |
59 |
60 |
64 |
65 |
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/popup/src/components/Streak.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 |
3 | import { UserStreakI } from '@/types/leetpush.interface.ts'
4 | import { formatStreak, getDayColor, streakEmoji } from '@/lib/utils.ts'
5 |
6 | export default function Streak({ data }: { data: UserStreakI }) {
7 | const endRef = useRef(null)
8 | const streakArray = formatStreak(data.fullSubmissionArray)
9 |
10 | useEffect(() => {
11 | if (endRef.current && streakArray.length > 0) {
12 | endRef.current.scrollLeft = endRef.current.scrollWidth
13 | }
14 | }, [streakArray])
15 |
16 | return (
17 |
18 |
19 | Your Max
20 | Streak: {data.maxStreak} {streakEmoji(96)}
21 |
22 |
23 |
25 | {streakArray.map((entry, i) => (
26 |
28 |
29 | {entry.days.map((daysEntry, i) => (
30 |
33 | ))}
34 |
35 |
{entry.month}
36 |
37 | ))}
38 |
39 |
40 | )
41 | }
42 |
43 |
44 | //res.map(entry => {
45 | // console.log(entry.month)
46 | //
47 | // entry.days.map(dayEntry => {
48 | // console.log(dayEntry)
49 | // })
50 | //
51 | // console.log('---')
52 | // })
53 |
--------------------------------------------------------------------------------
/popup/src/components/Welcome.tsx:
--------------------------------------------------------------------------------
1 | interface WelcomeProps {
2 | username: string
3 | totalProblems: number | undefined
4 | }
5 |
6 | export default function Welcome({ username, totalProblems }: WelcomeProps) {
7 | return (
8 |
9 |
10 | Hi, {username}
11 |
12 |
13 |
14 | Total
15 | Solved: {totalProblems}
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/popup/src/components/ui/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import { Spin } from 'antd'
2 | import { LoadingOutlined } from '@ant-design/icons'
3 |
4 | export default function Spinner() {
5 | return (
6 |
7 | }
9 | size="large" />
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/popup/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/popup/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/popup/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/popup/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/popup/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const labelVariants = cva(
8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
9 | )
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ))
22 | Label.displayName = LabelPrimitive.Root.displayName
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/popup/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ProgressPrimitive from "@radix-ui/react-progress"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Progress = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, value, ...props }, ref) => (
10 |
18 |
22 |
23 | ))
24 | Progress.displayName = ProgressPrimitive.Root.displayName
25 |
26 | export { Progress }
27 |
--------------------------------------------------------------------------------
/popup/src/context/userContext.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useState, createContext, useEffect } from 'react'
2 |
3 | interface UserContextI {
4 | username: string;
5 | setUsername: (username: string) => void;
6 | }
7 |
8 | const UserContext = createContext({
9 | username: '',
10 | setUsername: () => {
11 | },
12 | })
13 |
14 | export default function UserProvider({ children }: { children: ReactNode }) {
15 | const [username, setUsernameState] = useState(() => {
16 | return localStorage.getItem('username') || ''
17 | })
18 |
19 | const setUsername = (newUsername: string) => {
20 | setUsernameState(newUsername)
21 | localStorage.setItem('username', newUsername)
22 | }
23 |
24 | useEffect(() => {
25 | const storedUsername = localStorage.getItem('username')
26 | if (storedUsername && storedUsername !== username) {
27 | setUsernameState(storedUsername)
28 | }
29 | }, [])
30 |
31 |
32 | return (
33 |
34 | {children}
35 |
36 | )
37 | }
38 |
39 | export { UserProvider, UserContext }
40 |
--------------------------------------------------------------------------------
/popup/src/data/links.tsx:
--------------------------------------------------------------------------------
1 | import { IoLogoGithub } from 'react-icons/io'
2 | import { FaBug, FaStar } from 'react-icons/fa'
3 | import { SiBuymeacoffee } from 'react-icons/si'
4 |
5 | export const LINKS = [
6 | {
7 | name: 'github',
8 | link: 'https://github.com/LeetPushExtension/LeetPush',
9 | icon: ,
10 | },
11 | {
12 | name: 'star',
13 | link: 'https://chromewebstore.google.com/detail/leetpush/gmagfdabfjaipjgdfgddjgongeemkalf?hl=en-GB&authuser=0',
14 | icon: ,
15 | },
16 | {
17 | name: 'bug',
18 | link: 'https://github.com/LeetPushExtension/LeetPush/issues/new/choose',
19 | icon: ,
20 | },
21 | {
22 | name: 'buymeacoffee',
23 | link: 'https://www.buymeacoffee.com/husamahmud',
24 | icon: ,
25 | },
26 | ]
27 |
--------------------------------------------------------------------------------
/popup/src/hooks/leetpush.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query'
2 |
3 | import {
4 | fetchDailyProblem,
5 | fetchUserStats,
6 | fetchUserStreak,
7 | } from '@/lib/leetpush.api.ts'
8 | import {
9 | DailyProblemI,
10 | UserStatsI,
11 | UserStreakI,
12 | } from '@/types/leetpush.interface.ts'
13 |
14 | /**
15 | * useDailyProblem - Fetch the daily problem from the LeetPush API
16 | **/
17 | export function useDailyProblem() {
18 | const {
19 | data,
20 | error,
21 | isLoading,
22 | } = useQuery({
23 | queryKey: ['dailyProblem'],
24 | queryFn: fetchDailyProblem,
25 | staleTime: 24 * 60 * 60 * 1000,
26 | gcTime: 24 * 60 * 60 * 1000,
27 | })
28 |
29 | return { data, error, isLoading }
30 | }
31 |
32 | /**
33 | * useUserStats - Fetch user stats from the LeetPush API
34 | * @param username - The username of the user to fetch stats for
35 | **/
36 | export function useUserStats(username: string) {
37 | const {
38 | data,
39 | error,
40 | isLoading,
41 | } = useQuery({
42 | queryKey: ['userStats', username],
43 | queryFn: () => fetchUserStats(username),
44 | })
45 |
46 | return { data, error, isLoading }
47 | }
48 |
49 | /**
50 | * useUserStreak - Fetch user streak from the LeetPush API
51 | * @param username - The username of the user to fetch streak for
52 | **/
53 | export function useUserStreak(username: string) {
54 | const {
55 | data,
56 | error,
57 | isLoading,
58 | } = useQuery({
59 | queryKey: ['userStreak', username],
60 | queryFn: () => fetchUserStreak(username),
61 | })
62 |
63 | return { data, error, isLoading }
64 | }
65 |
--------------------------------------------------------------------------------
/popup/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | width: 500px;
8 |
9 | --background: 0 0% 100%;
10 | --foreground: 222.2 84% 4.9%;
11 | --card: 0 0% 100%;
12 | --card-foreground: 222.2 84% 4.9%;
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 84% 4.9%;
15 | --primary: 222.2 47.4% 11.2%;
16 | --primary-foreground: 210 40% 98%;
17 | --secondary: 210 40% 96.1%;
18 | --secondary-foreground: 222.2 47.4% 11.2%;
19 | --muted: 210 40% 96.1%;
20 | --muted-foreground: 215.4 16.3% 46.9%;
21 | --accent: 210 40% 96.1%;
22 | --accent-foreground: 222.2 47.4% 11.2%;
23 | --destructive: 0 84.2% 60.2%;
24 | --destructive-foreground: 210 40% 98%;
25 | --border: 214.3 31.8% 91.4%;
26 | --input: 214.3 31.8% 91.4%;
27 | --ring: 222.2 84% 4.9%;
28 | --radius: 0.5rem;
29 | --chart-1: 12 76% 61%;
30 | --chart-2: 173 58% 39%;
31 | --chart-3: 197 37% 24%;
32 | --chart-4: 43 74% 66%;
33 | --chart-5: 27 87% 67%;
34 | }
35 |
36 | .dark {
37 | --background: 222.2 84% 4.9%;
38 | --foreground: 210 40% 98%;
39 | --card: 222.2 84% 4.9%;
40 | --card-foreground: 210 40% 98%;
41 | --popover: 222.2 84% 4.9%;
42 | --popover-foreground: 210 40% 98%;
43 | --primary: 210 40% 98%;
44 | --primary-foreground: 222.2 47.4% 11.2%;
45 | --secondary: 217.2 32.6% 17.5%;
46 | --secondary-foreground: 210 40% 98%;
47 | --muted: 217.2 32.6% 17.5%;
48 | --muted-foreground: 215 20.2% 65.1%;
49 | --accent: 217.2 32.6% 17.5%;
50 | --accent-foreground: 210 40% 98%;
51 | --destructive: 0 62.8% 30.6%;
52 | --destructive-foreground: 210 40% 98%;
53 | --border: 217.2 32.6% 17.5%;
54 | --input: 217.2 32.6% 17.5%;
55 | --ring: 212.7 26.8% 83.9%;
56 | --chart-1: 220 70% 50%;
57 | --chart-2: 160 60% 45%;
58 | --chart-3: 30 80% 55%;
59 | --chart-4: 280 65% 60%;
60 | --chart-5: 340 75% 55%;
61 | }
62 | }
63 |
64 | * {
65 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
66 | }
67 |
68 | .scrollbar-hidden::-webkit-scrollbar {
69 | display: none;
70 | scrollbar-width: none;
71 | -ms-overflow-style: none;
72 | }
73 |
--------------------------------------------------------------------------------
/popup/src/lib/leetpush.api.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DailyProblemI,
3 | UserStatsI,
4 | UserStreakI,
5 | } from '@/types/leetpush.interface.ts'
6 |
7 | /**
8 | * fetchDailyProblem - Fetch the daily problem from the LeetPush API
9 | * @returns The daily problem
10 | **/
11 | export const fetchDailyProblem = async (): Promise => {
12 | const response = await fetch(`https://leet-push-api-git-master-husamahmuds-projects.vercel.app/api/v2/daily`)
13 | if (!response.ok) throw new Error('Failed to fetch the daily problem')
14 |
15 | const data = await response.json()
16 | return data.data
17 | }
18 |
19 | /**
20 | * fetchUserStats - Fetch user stats from the LeetPush API
21 | * @param username - The username of the user to fetch stats for
22 | * @returns The user stats
23 | **/
24 | export const fetchUserStats = async (username: string): Promise => {
25 | const response = await fetch(`https://leet-push-api-git-master-husamahmuds-projects.vercel.app/api/v2/${username}`)
26 | if (response.status === 404) throw new Error('User not found')
27 | else if (!response.ok) throw new Error('Failed to fetch user stats')
28 |
29 | const data = await response.json()
30 | return data.data
31 | }
32 |
33 | /**
34 | * fetchUserStreak - Fetch user streak from the LeetPush API
35 | * @param username - The username of the user to fetch streak for
36 | **/
37 | export const fetchUserStreak = async (username: string): Promise => {
38 | const response = await fetch(`https://leet-push-api-git-master-husamahmuds-projects.vercel.app/api/v2/userProfileCalendar/${username}`)
39 | if (!response.ok) throw new Error('Failed to fetch user streak')
40 |
41 | return await response.json()
42 | }
43 |
--------------------------------------------------------------------------------
/popup/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from 'clsx'
2 | import { twMerge } from 'tailwind-merge'
3 | import { DayValueI, StreakI } from '@/types/leetpush.interface.ts'
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs))
7 | }
8 |
9 | export function formatDate(date: Date): string {
10 | return date.toISOString().split('T')[0]
11 | }
12 |
13 | export function getDifficultyColor(difficulty: 'Easy' | 'Medium' | 'Hard'): string {
14 | switch (difficulty) {
15 | case 'Easy':
16 | return 'bg-lp-green-dark bg-lp-green'
17 | case 'Medium':
18 | return 'bg-lp-yellow-dark bg-lp-yellow'
19 | case 'Hard':
20 | return 'bg-lp-red-dark bg-lp-red'
21 | default:
22 | return 'text-gray-500 bg-gray-100'
23 | }
24 | }
25 |
26 | export function streakEmoji(streakLength: number): string {
27 | if (streakLength === 1) return '🌱'
28 | else if (streakLength <= 5) return '🌿'
29 | else if (streakLength <= 10) return '🔥'
30 | else if (streakLength <= 20) return '🌟'
31 | else if (streakLength <= 30) return '💪'
32 | else if (streakLength <= 40) return '🚀'
33 | else if (streakLength <= 50) return '🎯'
34 | else if (streakLength <= 75) return '🏆'
35 | else if (streakLength <= 100) return '👑'
36 | else return '🐉'
37 | }
38 |
39 | export function getDayColor(count: number) {
40 | if (count === 0) return '#ffffff14'
41 | if (count < 5) return '#016620'
42 | if (count < 10) return '#28c244'
43 | if (count < 15) return '#67BD72'
44 | return '#9be9a8'
45 | }
46 |
47 | export function formatStreak(data: StreakI[]) {
48 | const result: { [key: string]: DayValueI[] } = {}
49 |
50 | data.forEach(entry => {
51 | const date = new Date(entry.date)
52 | const month = date.toLocaleString('default', { month: 'short' })
53 | const day = date.getDate()
54 |
55 | if (!result[month]) result[month] = []
56 |
57 | result[month].push({ day, value: entry.value })
58 | })
59 |
60 | return Object.keys(result).map(month => ({
61 | month,
62 | days: result[month],
63 | }))
64 | }
65 |
--------------------------------------------------------------------------------
/popup/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
5 |
6 | import App from './components/App.tsx'
7 | import UserProvider from '@/context/userContext.tsx'
8 | import './index.css'
9 |
10 | const queryClient = new QueryClient()
11 |
12 | createRoot(document.getElementById('root')!).render(
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ,
21 | )
22 |
--------------------------------------------------------------------------------
/popup/src/types/leetpush.interface.ts:
--------------------------------------------------------------------------------
1 | export interface DailyProblemI {
2 | date: string
3 | link: string
4 | question: {
5 | questionId: string
6 | questionFrontendId: string
7 | title: string
8 | titleSlug: string
9 | difficulty: string
10 | topicTags: {
11 | name: string
12 | slug: string
13 | }[]
14 | stats: string
15 | }
16 | }
17 |
18 | export interface UserStatsI {
19 | allQuestionsCount: {
20 | difficulty: string
21 | count: number
22 | }[]
23 | acSubmissionNum: {
24 | difficulty: string
25 | count: number
26 | }[]
27 | }
28 |
29 | export interface UserStreakI {
30 | maxStreak: number
31 | totalActiveDays: number
32 | fullSubmissionArray: {
33 | date: string
34 | value: number
35 | }[]
36 | }
37 |
38 | export interface StreakI {
39 | date: string
40 | value: number
41 | }
42 |
43 | export interface DayValueI {
44 | day: number;
45 | value: number;
46 | }
47 |
--------------------------------------------------------------------------------
/popup/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/popup/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ['class'],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | prefix: '',
11 | theme: {
12 | container: {
13 | center: true,
14 | padding: '2rem',
15 | screens: {
16 | '2xl': '1400px',
17 | },
18 | },
19 | extend: {
20 | colors: {
21 | 'lp-yellow': 'rgb(255 192 30)',
22 | 'lp-yellow-dark': '#ffc01e40',
23 | 'lp-green': 'rgb(0 184 163)',
24 | 'lp-green-dark': '#2cbb5d40',
25 | 'lp-green-bg': 'rgb(44 187 93)',
26 | 'lp-red': 'rgb(255 55 95)',
27 | 'lp-red-dark': '#ef474340',
28 | 'lp-white': '#e7e7e7',
29 | 'lp-grey': '#eff1f6bf',
30 | 'lp-greyer': '#ebebf54d',
31 | 'lp-dark': '#ffffff1a',
32 | border: 'hsl(var(--border))',
33 | input: 'hsl(var(--input))',
34 | ring: 'hsl(var(--ring))',
35 | background: 'hsl(var(--background))',
36 | foreground: 'hsl(var(--foreground))',
37 | primary: {
38 | DEFAULT: 'hsl(var(--primary))',
39 | foreground: 'hsl(var(--primary-foreground))',
40 | },
41 | secondary: {
42 | DEFAULT: 'hsl(var(--secondary))',
43 | foreground: 'hsl(var(--secondary-foreground))',
44 | },
45 | destructive: {
46 | DEFAULT: 'hsl(var(--destructive))',
47 | foreground: 'hsl(var(--destructive-foreground))',
48 | },
49 | muted: {
50 | DEFAULT: 'hsl(var(--muted))',
51 | foreground: 'hsl(var(--muted-foreground))',
52 | },
53 | accent: {
54 | DEFAULT: 'hsl(var(--accent))',
55 | foreground: 'hsl(var(--accent-foreground))',
56 | },
57 | popover: {
58 | DEFAULT: 'hsl(var(--popover))',
59 | foreground: 'hsl(var(--popover-foreground))',
60 | },
61 | card: {
62 | DEFAULT: 'hsl(var(--card))',
63 | foreground: 'hsl(var(--card-foreground))',
64 | },
65 | },
66 | borderRadius: {
67 | lg: 'var(--radius)',
68 | md: 'calc(var(--radius) - 2px)',
69 | sm: 'calc(var(--radius) - 4px)',
70 | },
71 | keyframes: {
72 | 'accordion-down': {
73 | from: { height: '0' },
74 | to: { height: 'var(--radix-accordion-content-height)' },
75 | },
76 | 'accordion-up': {
77 | from: { height: 'var(--radix-accordion-content-height)' },
78 | to: { height: '0' },
79 | },
80 | },
81 | animation: {
82 | 'accordion-down': 'accordion-down 0.2s ease-out',
83 | 'accordion-up': 'accordion-up 0.2s ease-out',
84 | },
85 | },
86 | },
87 | plugins: [require('tailwindcss-animate')],
88 | }
89 |
--------------------------------------------------------------------------------
/popup/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true,
22 |
23 | "baseUrl": ".",
24 | "paths": {
25 | "@/*": [
26 | "./src/*"
27 | ]
28 | }
29 | },
30 | "include": ["src"]
31 | }
32 |
--------------------------------------------------------------------------------
/popup/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ],
7 | "compilerOptions": {
8 | "baseUrl": ".",
9 | "paths": {
10 | "@/*": ["./src/*"]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/popup/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "lib": ["ES2023"],
5 | "module": "ESNext",
6 | "skipLibCheck": true,
7 |
8 | /* Bundler mode */
9 | "moduleResolution": "bundler",
10 | "allowImportingTsExtensions": true,
11 | "isolatedModules": true,
12 | "moduleDetection": "force",
13 | "noEmit": true,
14 |
15 | /* Linting */
16 | "strict": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noFallthroughCasesInSwitch": true
20 | },
21 | "include": ["vite.config.ts"]
22 | }
23 |
--------------------------------------------------------------------------------
/popup/vite.config.ts:
--------------------------------------------------------------------------------
1 | import react from '@vitejs/plugin-react-swc'
2 | import { defineConfig } from 'vite'
3 | import path from 'path'
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | plugins: [react()],
8 | base: './',
9 | resolve: {
10 | alias: {
11 | '@': path.resolve(__dirname, './src'),
12 | },
13 | },
14 | })
15 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | /** LeetPush Button Styles **/
2 | #leetpush-div,
3 | #leetpush-div-edit,
4 | #leetpush-div-CodeEditor,
5 | #leetpush-div-edit-CodeEditor {
6 | display: flex;
7 | align-items: center;
8 | text-align: center;
9 | font-size: 13px;
10 | transition: all 0.2s ease;
11 | position: relative;
12 | }
13 |
14 | /** ToolBar Layout **/
15 | /* Push Button */
16 | #leetpush-div-CodeEditor {
17 | background-color: transparent !important;
18 | margin-left: 0 !important;
19 | border-radius: 0 !important;
20 | }
21 |
22 | #leetpush-div-CodeEditor:hover {
23 | background-color: rgb(226 181 92 / 10%) !important;
24 | }
25 |
26 | #leetpush-btn-CodeEditor {
27 | color: #E7A41F !important;
28 | padding-inline: .75rem !important;
29 | }
30 |
31 | /* Edit Button */
32 | #leetpush-div-edit-CodeEditor {
33 | background-color: transparent !important;
34 | margin-left: 0 !important;
35 | border-radius: 0 !important;
36 | }
37 |
38 | #leetpush-div-edit-CodeEditor:hover {
39 | background-color: #3e3e3e !important;
40 | }
41 |
42 | #leetpush-btn-edit-CodeEditor{
43 | color: #fff9 !important;
44 | padding-inline: .75rem !important;
45 | }
46 |
47 | /** CodeEditor Layout **/
48 |
49 | #leetpush-div-edit {
50 | margin-left: 0.5rem !important;
51 | }
52 |
53 | /* No background for toolbar */
54 | #leetpush-div,
55 | #leetpush-div-edit {
56 | border: none;
57 | padding: 0 !important;
58 | }
59 |
60 | /* Base button styles */
61 | #leetpush-btn,
62 | #leetpush-btn-CodeEditor,
63 | #leetpush-btn-edit,
64 | #leetpush-btn-edit-CodeEditor {
65 | line-height: 18px;
66 | font-weight: 600;
67 | user-select: none;
68 | border: none;
69 | display: flex;
70 | align-items: center;
71 | gap: 4px;
72 | cursor: pointer;
73 | }
74 |
75 | /* Edit button colors */
76 | #leetpush-btn-edit,
77 | #leetpush-btn-edit-CodeEditor {
78 | color: rgba(255, 255, 255, 0.8);
79 | font-size: 13px;
80 | }
81 |
82 | #leetpush-btn-edit {
83 | padding-block: 0;
84 | }
85 |
86 | #leetpush-btn-edit-CodeEditor {
87 | padding-block: 0.25rem;
88 | }
89 |
90 | /* Icon for CodeEditor push button */
91 | #leetpush-btn-CodeEditor::before {
92 | content: "";
93 | display: inline-block;
94 | width: 16px;
95 | height: 16px;
96 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30' fill='%23e7a41e'%3E%3Cpath d='M15,3C8.373,3,3,8.373,3,15c0,5.623,3.872,10.328,9.092,11.63C12.036,26.468,12,26.28,12,26.047v-2.051 c-0.487,0-1.303,0-1.508,0c-0.821,0-1.551-0.353-1.905-1.009c-0.393-0.729-0.461-1.844-1.435-2.526 c-0.289-0.227-0.069-0.486,0.264-0.451c0.615,0.174,1.125,0.596,1.605,1.222c0.478,0.627,0.703,0.769,1.596,0.769 c0.433,0,1.081-0.025,1.691-0.121c0.328-0.833,0.895-1.6,1.588-1.962c-3.996-0.411-5.903-2.399-5.903-5.098 c0-1.162,0.495-2.286,1.336-3.233C9.053,10.647,8.706,8.73,9.435,8c1.798,0,2.885,1.166,3.146,1.481C13.477,9.174,14.461,9,15.495,9 c1.036,0,2.024,0.174,2.922,0.483C18.675,9.17,19.763,8,21.565,8c0.732,0.731,0.381,2.656,0.102,3.594 c0.836,0.945,1.328,2.066,1.328,3.226c0,2.697-1.904,4.684-5.894,5.097C18.199,20.49,19,22.1,19,23.313v2.734 c0,0.104-0.023,0.179-0.035,0.268C23.641,24.676,27,20.236,27,15C27,8.373,21.627,3,15,3z'%3E%3C/path%3E%3C/svg%3E");
97 | background-size: cover;
98 | }
99 |
100 | /* Icon for CodeEditor edit button */
101 | #leetpush-btn-edit-CodeEditor::before {
102 | content: "";
103 | display: inline-block;
104 | width: 16px;
105 | height: 16px;
106 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='rgba(255, 255, 255, 0.8)'%3E%3Cpath d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z'%3E%3C/path%3E%3C/svg%3E");
107 | background-size: cover;
108 | }
109 |
110 | /* No icons for toolbar buttons */
111 | #leetpush-btn::before,
112 | #leetpush-btn-edit::before {
113 | display: none;
114 | }
115 |
116 | /* Disabled button state */
117 | #leetpush-btn:disabled,
118 | #leetpush-btn-CodeEditor:disabled,
119 | #leetpush-btn-edit:disabled,
120 | #leetpush-btn-edit-CodeEditor:disabled {
121 | opacity: 0.5;
122 | cursor: not-allowed;
123 | }
124 |
125 | /* Different states for buttons */
126 | #leetpush-btn.loading::before,
127 | #leetpush-btn-CodeEditor.loading::before {
128 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23e7a41e'%3E%3Cpath d='M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8Z'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='1s' repeatCount='indefinite'/%3E%3C/path%3E%3C/svg%3E");
129 | display: inline-block;
130 | }
131 |
132 | #leetpush-btn.error::before,
133 | #leetpush-btn-CodeEditor.error::before {
134 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23e74c3c'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z'%3E%3C/path%3E%3C/svg%3E");
135 | display: inline-block;
136 | }
137 |
138 | /* Success state with checkmark on the right */
139 | #leetpush-btn.success,
140 | #leetpush-btn-CodeEditor.success {
141 | position: relative;
142 | color: #2ecc71;
143 | }
144 |
145 | #leetpush-btn.success::before,
146 | #leetpush-btn-CodeEditor.success::before {
147 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30' fill='%232ecc71'%3E%3Cpath d='M15,3C8.373,3,3,8.373,3,15c0,5.623,3.872,10.328,9.092,11.63C12.036,26.468,12,26.28,12,26.047v-2.051 c-0.487,0-1.303,0-1.508,0c-0.821,0-1.551-0.353-1.905-1.009c-0.393-0.729-0.461-1.844-1.435-2.526 c-0.289-0.227-0.069-0.486,0.264-0.451c0.615,0.174,1.125,0.596,1.605,1.222c0.478,0.627,0.703,0.769,1.596,0.769 c0.433,0,1.081-0.025,1.691-0.121c0.328-0.833,0.895-1.6,1.588-1.962c-3.996-0.411-5.903-2.399-5.903-5.098 c0-1.162,0.495-2.286,1.336-3.233C9.053,10.647,8.706,8.73,9.435,8c1.798,0,2.885,1.166,3.146,1.481C13.477,9.174,14.461,9,15.495,9 c1.036,0,2.024,0.174,2.922,0.483C18.675,9.17,19.763,8,21.565,8c0.732,0.731,0.381,2.656,0.102,3.594 c0.836,0.945,1.328,2.066,1.328,3.226c0,2.697-1.904,4.684-5.894,5.097C18.199,20.49,19,22.1,19,23.313v2.734 c0,0.104-0.023,0.179-0.035,0.268C23.641,24.676,27,20.236,27,15C27,8.373,21.627,3,15,3z'%3E%3C/path%3E%3C/svg%3E");
148 | display: inline-block;
149 | }
150 |
151 | #leetpush-btn.success::after,
152 | #leetpush-btn-CodeEditor.success::after {
153 | display: none;
154 | }
155 |
156 | /** LeetPush Modal **/
157 | #lp-modal {
158 | width: 100%;
159 | height: 100%;
160 | position: fixed;
161 | z-index: 999999;
162 | top: 0;
163 | left: 0;
164 | display: flex;
165 | align-items: center;
166 | justify-content: center;
167 | background-color: rgba(0, 0, 0, 0.6);
168 | animation: fadeIn 0.2s ease;
169 | }
170 |
171 | @keyframes fadeIn {
172 | from {
173 | opacity: 0;
174 | }
175 | to {
176 | opacity: 1;
177 | }
178 | }
179 |
180 | #lp-container {
181 | width: 350px;
182 | max-height: 90vh;
183 | overflow-y: auto;
184 | background-color: rgb(48, 48, 48);
185 | box-shadow: 0 1px 3px #0000003d, 0 10px 28px -4px #0000007a;
186 | border-radius: 1rem;
187 | padding: 1.5rem 2rem 2rem;
188 | display: flex;
189 | flex-direction: column;
190 | justify-content: space-between;
191 | animation: slideIn 0.3s ease;
192 | }
193 |
194 | @keyframes slideIn {
195 | from {
196 | transform: translateY(-20px);
197 | opacity: 0;
198 | }
199 | to {
200 | transform: translateY(0);
201 | opacity: 1;
202 | }
203 | }
204 |
205 | #lp-close-btn {
206 | display: flex;
207 | justify-content: flex-end;
208 | font-size: 1rem;
209 | }
210 |
211 | #lp-close-btn > button {
212 | background: none;
213 | border: none;
214 | color: #999;
215 | font-size: 1.2rem;
216 | cursor: pointer;
217 | padding: 5px;
218 | border-radius: 50%;
219 | transition: background-color 0.2s ease;
220 | }
221 |
222 | #lp-close-btn > button:hover {
223 | color: #fff;
224 | }
225 |
226 | #lp-container h3 {
227 | color: white;
228 | text-align: center;
229 | font-size: 24px;
230 | padding-bottom: 1rem;
231 | font-weight: bold;
232 | margin-top: 0;
233 | }
234 |
235 | #lp-container h3 span {
236 | color: #e7a41e;
237 | }
238 |
239 | #lp-form {
240 | display: flex;
241 | flex-direction: column;
242 | justify-content: center;
243 | gap: 1.3rem;
244 | }
245 |
246 | .lp-div {
247 | display: flex;
248 | flex-direction: column;
249 | gap: .5rem;
250 | }
251 |
252 | #lp-form input[type="text"] {
253 | width: 100%;
254 | border-radius: 0.2rem;
255 | padding: 0.5rem;
256 | margin: 0;
257 | border: none;
258 | outline: none;
259 | background-color: white;
260 | font-family: inherit;
261 | font-size: inherit;
262 | color: black;
263 | box-sizing: border-box;
264 | }
265 |
266 | #lp-form input[type="text"]:focus {
267 | box-shadow: 0 0 0 2px #e7a41e;
268 | }
269 |
270 | .lp-div:nth-child(2) label {
271 | display: flex;
272 | justify-content: space-between;
273 | align-items: center;
274 | }
275 |
276 | .lp-div:nth-child(2) label a {
277 | font-size: 10px;
278 | text-decoration: underline;
279 | color: #e7a41e;
280 | }
281 |
282 | .lp-div:nth-child(2) label a:hover {
283 | color: #e2b65c;
284 | }
285 |
286 | #lp-radios {
287 | display: flex;
288 | justify-content: space-evenly;
289 | margin-top: 0.5rem;
290 | }
291 |
292 | .radio-div {
293 | display: flex;
294 | align-items: center;
295 | gap: 0.5rem;
296 | }
297 |
298 | #lp-submit-btn {
299 | border: none;
300 | cursor: pointer;
301 | background-color: #e7a41e;
302 | border-radius: 0.5rem;
303 | line-height: 18px;
304 | font-weight: 600;
305 | font-size: 13px;
306 | padding: 0.375rem 1.25rem;
307 | user-select: none;
308 | height: 32px;
309 | color: #070706;
310 | transition: background-color 0.2s ease;
311 | margin-top: 0.5rem;
312 | }
313 |
314 | #lp-submit-btn:hover {
315 | background-color: #e2b65c;
316 | }
317 |
318 | /* Labels */
319 | #lp-form label {
320 | color: rgba(255, 255, 255, 0.8);
321 | font-size: 14px;
322 | }
323 |
324 | /* Radio buttons */
325 | #lp-form input[type="radio"] {
326 | accent-color: #e7a41e;
327 | }
328 |
329 | /* Shortcut Configuration Styles */
330 | .lp-keyboard-shortcut {
331 | display: flex;
332 | flex-direction: column;
333 | gap: 0.5rem;
334 | }
335 |
336 | .shortcut-config {
337 | display: flex;
338 | align-items: center;
339 | gap: 8px;
340 | margin-top: 0.5rem;
341 | }
342 |
343 | #shortcut-key {
344 | width: 40px;
345 | text-align: center;
346 | text-transform: uppercase;
347 | border-radius: 0.2rem;
348 | padding: 0.5rem;
349 | border: none;
350 | outline: none;
351 | background-color: white;
352 | font-family: inherit;
353 | font-size: inherit;
354 | color: black;
355 | box-sizing: border-box;
356 | }
357 |
358 | #shortcut-key:focus {
359 | box-shadow: 0 0 0 2px #e7a41e;
360 | }
361 |
362 | #shortcut-modifier {
363 | min-width: 120px;
364 | border-radius: 0.2rem;
365 | padding: 0.5rem;
366 | border: none;
367 | outline: none;
368 | background-color: white;
369 | font-family: inherit;
370 | font-size: inherit;
371 | color: black;
372 | box-sizing: border-box;
373 | }
374 |
375 | #shortcut-modifier:focus {
376 | box-shadow: 0 0 0 2px #e7a41e;
377 | }
378 |
379 | .shortcut-config span {
380 | color: rgba(255, 255, 255, 0.8);
381 | font-weight: bold;
382 | }
383 |
384 | /* Improve the dropdown appearance */
385 | #shortcut-modifier option {
386 | background-color: white;
387 | color: black;
388 | padding: 5px;
389 | }
390 |
391 | /* Add a custom focus style for keyboard navigation */
392 | #shortcut-modifier:focus,
393 | #shortcut-key:focus {
394 | box-shadow: 0 0 0 2px #e7a41e;
395 | outline: none;
396 | }
397 |
398 | /* Hover effects */
399 | #shortcut-modifier:hover,
400 | #shortcut-key:hover {
401 | background-color: #f5f5f5;
402 | }
403 |
404 | /* Animation for shortcut section */
405 | .lp-keyboard-shortcut {
406 | animation: fadeIn 0.4s ease;
407 | animation-delay: 0.2s;
408 | animation-fill-mode: both;
409 | }
410 |
411 | /* Responsive adjustments for smaller screens */
412 | @media (max-width: 400px) {
413 | .shortcut-config {
414 | flex-direction: column;
415 | align-items: flex-start;
416 | gap: 5px;
417 | }
418 |
419 | #shortcut-modifier {
420 | width: 100%;
421 | min-width: unset;
422 | }
423 |
424 | #shortcut-key {
425 | width: 100%;
426 | }
427 |
428 | .shortcut-config span {
429 | display: none;
430 | }
431 | }
432 |
433 |
--------------------------------------------------------------------------------