diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7290ded..eaf76e3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -132,3 +132,4 @@ jobs: path: coverage/ retention-days: 30 + diff --git a/README.md b/README.md index ee03bd6..ebfdb80 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,18 @@ # 🤝 Leetcode Buddy -Your daily LeetCode companion! A Chrome extension that helps you complete the NeetCode 250 problem list in order by restricting access to non-whitelisted websites until you solve the current problem. +Your daily LeetCode companion! A Chrome extension that helps you stay focused and complete coding problem sets by restricting access to non-whitelisted websites until you solve your daily problem. ## Features -- ✅ **Automatic Problem Tracking**: Detects when you solve problems on LeetCode -- 🚫 **Website Blocking**: Blocks all websites except neetcode.io, leetcode.com, and chatgpt.com -- 🎯 **Enforced Order**: Redirects you to the next unsolved problem in the NeetCode 250 list +- ✅ **Automatic Problem Tracking**: Detects when you solve problems on LeetCode and automatically advances to the next one +- 🚫 **Website Blocking**: Redirects non-whitelisted websites to your current problem until you solve it +- 🎯 **Multiple Problem Sets**: Choose from Blind 75, NeetCode 150, NeetCode 250, or NeetCode All +- 🎲 **Random Problem Selection**: Option to randomly select from unsolved problems instead of going in sequence +- 📊 **Progress Tracking**: Visual progress bars showing your progress overall and by category - ⏱️ **Timed Bypass**: Take a 10-minute break when needed (30-minute cooldown) -- 📊 **Progress Tracking**: Visual progress bar showing how many problems you've solved - 🔄 **Auto-Sync**: Automatically syncs with your LeetCode account status +- 🎉 **Celebrations**: Optional confetti animations when you solve your daily problem +- ⚙️ **Customizable Exclusions**: Add up to 10 websites to exclude from redirection ## Installation @@ -24,53 +27,43 @@ Your daily LeetCode companion! A Chrome extension that helps you complete the Ne ### Step 2: Log in to LeetCode 1. Navigate to [leetcode.com](https://leetcode.com/) -2. Log in to your account (adamschmidt2023) +2. Log in to your LeetCode account 3. The extension will automatically sync your solved problems -### Step 3: Start Grinding! +### Step 3: Configure Settings (Optional) -Once installed, the extension will: +1. Right-click the extension icon and select **Options** (or click the ⚙️ Settings button in the popup) +2. Choose your preferred problem set (Blind 75, NeetCode 150, NeetCode 250, or NeetCode All) +3. Customize your preferences (see [Settings & Configuration](#settings--configuration) below) + +### Step 4: Start Grinding! -- Redirect all non-whitelisted websites to your current NeetCode 250 problem +Once installed, the extension will: +- Redirect all non-whitelisted websites to your current problem - Track your progress automatically - Update to the next problem when you solve the current one +- Unblock all websites after you solve your daily problem (until midnight) -## How It Works - -### Website Blocking - -The extension uses Chrome's `declarativeNetRequest` API to redirect all navigation to non-whitelisted websites. Only these domains are allowed: +## How to Use -- `neetcode.io` - View the NeetCode 250 list -- `leetcode.com` - Solve problems -- `chatgpt.com` - Get help when stuck - -### Automatic Problem Detection - -When you're on a LeetCode problem page, the content script: - -1. Monitors the page for successful submission indicators -2. Queries LeetCode's GraphQL API to confirm the problem status -3. Notifies the background service worker when status is "Accepted" -4. Automatically advances to the next problem in the list - -### Progress Tracking - -Your progress is stored in Chrome's sync storage and automatically backed up across devices. The extension: - -- Tracks which problems you've solved -- Maintains your current position in the NeetCode 250 list -- Syncs with LeetCode's API on startup to ensure accuracy - -## Using the Extension - -### View Progress +### View Your Progress Click the extension icon in your Chrome toolbar to see: - -- Total problems solved (X / 250) -- Current problem name and link -- Visual progress bar +- **Overall Progress**: Total problems solved with a visual progress bar +- **Current Problem**: The problem you need to solve next, with direct links to LeetCode and NeetCode video solutions +- **Category Progress**: Breakdown of solved problems by category +- **Daily Status**: Whether you've completed today's problem + +### Solve Your Daily Problem + +1. The extension automatically redirects you to your current problem when you try to visit non-whitelisted sites +2. Work on the problem on LeetCode +3. Submit your solution +4. Once your submission is marked as "Accepted", the extension will: + - Show a celebration animation (if enabled) + - Mark the problem as solved + - Advance to the next problem + - Unblock all websites until midnight ### Take a Break @@ -82,13 +75,104 @@ When you need a breather: 4. After the break, the redirect automatically resumes 5. You can take another break after a 30-minute cooldown -### Refresh Status +### Refresh Your Status If you've solved problems outside the extension or want to force a sync: 1. Click the extension icon 2. Click **"🔄 Refresh Status"** -3. The extension will query LeetCode for your latest progress +3. The extension will query LeetCode for your latest progress and update accordingly + +### Access Settings + +1. Click the extension icon +2. Click **"⚙️ Settings"** to open the options page +3. Configure your preferences (see [Settings & Configuration](#settings--configuration) below) + +## Settings & Configuration + +Access settings by right-clicking the extension icon and selecting **Options**, or click the ⚙️ Settings button in the popup. + +### Problem Set Selection + +Choose which problem set you want to work through: +- **Blind 75**: The classic 75 essential problems +- **NeetCode 150**: 150 curated problems +- **NeetCode 250**: 250 problems organized by category +- **NeetCode All**: Comprehensive collection of all NeetCode problems + +### Display Preferences + +- **Celebration Animations**: Toggle confetti and celebration animations when you solve your daily problem +- **Sort Problems by Difficulty**: Sort problems within each category by difficulty (Easy → Medium → Hard) instead of the original problemset order +- **Random Problem Selection**: Select problems randomly from unsolved problems instead of going in sequence +- **Clear Editor on First Open** (Experimental): Clear the code editor content when you first open a problem each day. Subsequent refreshes will preserve your work. + +### Exclusion List + +Customize which websites are excluded from redirection: + +- **System Domains** (Always Excluded): These domains are required for the extension to function: + - `leetcode.com` - Solve problems + - `neetcode.io` - View problem lists and solutions + - `accounts.google.com` - Google OAuth authentication + +- **Your Custom Domains**: Add up to 10 additional websites to exclude from redirection: + 1. Enter a domain (e.g., `github.com`) in the input field + 2. Click **"Add Domain"** + 3. Remove domains by clicking the × button next to them + 4. Click **"Reset to Defaults"** to restore default exclusions (GitHub, LinkedIn) + +### Category Progress + +View all problems organized by category with visual progress indicators. Click categories to expand and see individual problems and their status. + +### Reset Progress + +If you want to start fresh: +1. Scroll to the **Reset Progress** section +2. Click **"Reset All Progress"** +3. Confirm the action (this cannot be undone) + +## Troubleshooting + +### Extension Not Redirecting + +1. Make sure you're logged in to LeetCode +2. Check that the extension is enabled in `chrome://extensions/` +3. Verify that you haven't already solved today's problem (websites are unblocked after solving) +4. Try clicking **"🔄 Refresh Status"** in the extension popup +5. Check the exclusion list in settings to ensure the site isn't excluded + +### Problem Not Advancing + +1. Ensure your submission is marked as "Accepted" on LeetCode +2. Wait a few seconds for the extension to detect the change +3. Try refreshing the LeetCode problem page +4. Click **"🔄 Refresh Status"** in the extension popup +5. Check the browser console for any error messages + +### Can't Access Excluded Sites + +1. Check your internet connection +2. Make sure the sites are using `https://` +3. Verify the domain is in your exclusion list (Settings → Exclusion List) +4. Try disabling and re-enabling the extension + +### Progress Not Syncing + +1. Make sure you're logged in to LeetCode +2. Click **"🔄 Refresh Status"** in the extension popup +3. Check that Chrome sync is enabled (for cross-device sync) +4. Verify your LeetCode account has the problems marked as solved + +### Celebration Not Showing + +1. Check Settings → Display Preferences → Celebration Animations is enabled +2. Make sure you're solving the expected problem (not a different one) +3. Verify the problem was solved today (not in the past) + +--- ## Architecture @@ -104,7 +188,7 @@ Leetcode Buddy uses a modular architecture with ES6 modules for better maintaina ### Content Script (4 modules) - `src/content/index.js` - Main entry point & DOM observation - `src/content/api.js` - LeetCode API layer -- `src/content/detector.js` - Problem solve detection logic +- `src/content/detector.js` - Problem solve detection logic with optimization guards - `src/content/ui.js` - Celebrations & notifications ### Shared @@ -128,12 +212,18 @@ leetcodeForcer/ │ │ ├── index.js │ │ ├── api.js │ │ ├── detector.js +│ │ ├── editor.js │ │ └── ui.js │ ├── shared/ # Shared constants │ │ └── constants.js │ └── assets/ # Icons, data, styles │ ├── icons/ │ ├── data/ +│ │ ├── blind75.json +│ │ ├── neetcode150.json +│ │ ├── neetcode250.json +│ │ ├── neetcodeAll.json +│ │ └── problemAliases.json │ └── styles/ ├── tests/ # Unit and integration tests │ ├── background/ @@ -147,6 +237,35 @@ leetcodeForcer/ ## Technical Details +### How It Works + +#### Website Blocking + +The extension uses Chrome's `declarativeNetRequest` API to redirect all navigation to non-excluded websites. The exclusion list consists of: +- System-enforced domains (LeetCode, NeetCode, Google OAuth) - always excluded +- User-defined domains (up to 10 custom domains) - configurable in settings + +#### Automatic Problem Detection + +When you're on a LeetCode problem page, the content script: + +1. Monitors the page for successful submission indicators (DOM mutations) +2. Queries LeetCode's GraphQL API to confirm the problem status +3. Verifies the submission was made today (not in the past) +4. Checks that it's the expected problem (not a different one) +5. Notifies the background service worker when status is "Accepted" +6. Automatically advances to the next problem in the list +7. Optimizes by skipping redundant checks once a problem is confirmed solved today + +#### Progress Tracking + +Your progress is stored in Chrome's sync storage and automatically backed up across devices. The extension: + +- Tracks which problems you've solved (stored as problem slugs) +- Maintains your current position in the selected problem set +- Syncs with LeetCode's API on startup and when manually refreshed +- Tracks daily solve status (resets at midnight) + ### Permissions The extension requires: @@ -157,71 +276,32 @@ The extension requires: ### APIs Used -- **LeetCode GraphQL API**: Check problem solve status +- **LeetCode GraphQL API**: Check problem solve status and fetch submission history - Endpoint: `https://leetcode.com/graphql` - - Query: `question(titleSlug: $slug) { status }` + - Queries: `questionStatus`, `recentSubmissionList`, `submissionList`, `globalData` - **LeetCode Problems API**: Bulk fetch all problem statuses - Endpoint: `https://leetcode.com/api/problems/all/` ### Storage - `chrome.storage.sync`: - - - `currentIndex` - Current position in NeetCode 250 list + - `currentProblemSlug` - Current problem slug + - `categoryIndex` - Current category index + - `problemIndex` - Current problem index within category - `solvedProblems` - Array of solved problem slugs + - `activeProblemSet` - Selected problem set ID + - `userExclusionList` - User-defined exclusion domains + - `randomProblemSelection` - Random selection toggle + - `sortByDifficulty` - Sort by difficulty toggle + - `clearEditorOnFirstOpen` - Clear editor toggle + - `celebrationEnabled` - Celebration animations toggle - `chrome.storage.local`: - `bypassUntil` - Timestamp when bypass expires - `nextBypassAllowed` - Timestamp when next bypass can be activated - -## Troubleshooting - -### Extension Not Redirecting - -1. Make sure you're logged in to LeetCode -2. Check that the extension is enabled in `chrome://extensions/` -3. Try clicking "Refresh Status" in the extension popup - -### Problem Not Advancing - -1. Ensure your submission is marked as "Accepted" on LeetCode -2. Wait a few seconds for the extension to detect the change -3. Try refreshing the LeetCode problem page -4. Click "Refresh Status" in the extension popup - -### Can't Access Whitelisted Sites - -1. Check your internet connection -2. Make sure the sites are using `https://` -3. Try disabling and re-enabling the extension - -## Customization - -### Add More Whitelisted Sites - -Edit `src/shared/constants.js`: - -```javascript -export const WHITELIST = [ - "leetcode.com", - "neetcode.io", - "chatgpt.com", - "your-site.com", -]; -``` - -### Change Bypass Duration - -Edit `src/shared/constants.js`: - -```javascript -export const BYPASS_DURATION_MS = 15 * 60 * 1000; // 15 minutes -export const COOLDOWN_DURATION_MS = 60 * 60 * 1000; // 60 minutes -``` - -### Update Problem List - -Replace `src/assets/data/neetcode250.json` with your custom problem list organized by categories. + - `dailySolveDate` - Date when daily problem was solved (YYYY-MM-DD) + - `dailySolveProblem` - Slug of the problem solved today + - `problemFirstOpened_*` - Per-problem first-open tracking for editor clearing ## Development @@ -269,23 +349,60 @@ See [docs/TESTING.md](docs/TESTING.md) for detailed testing guide. - **Background Script**: `chrome://extensions/` → Click "service worker" under the extension - **Content Script**: Open DevTools on any LeetCode problem page - **Popup**: Right-click the extension icon → "Inspect popup" +- **Options Page**: Right-click the extension icon → "Options" → Open DevTools ### Contributing See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for development guidelines and best practices. +## Customization + +### Add More Excluded Sites + +Use the Settings page (Options → Exclusion List) to add up to 10 custom domains. For system-level changes, edit `src/shared/constants.js`: + +```javascript +export const DEFAULT_USER_EXCLUSION_LIST = [ + "github.com", + "linkedin.com", + "your-site.com", +]; +``` + +### Change Bypass Duration + +Edit `src/shared/constants.js`: + +```javascript +export const BYPASS_DURATION_MS = 15 * 60 * 1000; // 15 minutes +export const COOLDOWN_DURATION_MS = 60 * 60 * 1000; // 60 minutes +``` + +### Update Problem Lists + +Problem sets are stored in JSON format in `src/assets/data/`: +- `blind75.json` +- `neetcode150.json` +- `neetcode250.json` +- `neetcodeAll.json` + +Each file contains an array of categories, with each category containing an array of problems with properties: `slug`, `leetcodeId`, `title`, `difficulty`, `category`. + +### Modify Problem Aliases + +Edit `src/assets/data/problemAliases.json` to add or modify problem slug aliases for better matching. + ## License MIT License - Feel free to modify and distribute ## Credits -- NeetCode 250 problem list curated by [NeetCode](https://neetcode.io/) +- NeetCode problem lists curated by [NeetCode](https://neetcode.io/) - Built with Chrome Extension Manifest V3 --- **Good luck with your grinding! 💪** -Remember: The only way out is through. Stay focused and you'll complete all 250 problems! - +Remember: The only way out is through. Stay focused and you'll complete all your problems! diff --git a/babel.config.js b/babel.config.js index 7d50e0c..025da85 100644 --- a/babel.config.js +++ b/babel.config.js @@ -12,3 +12,4 @@ module.exports = { ] }; + diff --git a/docs/API.md b/docs/API.md index 762ccb9..e5004ab 100644 --- a/docs/API.md +++ b/docs/API.md @@ -890,3 +890,4 @@ All async functions return sensible defaults on error: Errors are logged to console for debugging. + diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 8db4a77..01469f4 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -465,3 +465,4 @@ By contributing, you agree that your contributions will be licensed under the MI Thank you for contributing to Leetcode Buddy! 🎉 + diff --git a/docs/TESTING.md b/docs/TESTING.md index ea0c7b6..e6823b3 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -490,3 +490,4 @@ See `.github/workflows/test.yml` for CI configuration. Tests run automatically o - [Testing Library](https://testing-library.com/docs/) - [Chrome Extension Testing](https://developer.chrome.com/docs/extensions/mv3/tut_testing/) + diff --git a/manifest.json b/manifest.json index 8f63c8c..d7b6449 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Leetcode Buddy", "version": "2.0.0", - "description": "Your daily LeetCode companion! Focuses on NeetCode 250 problems organized by category. Only allows neetcode.io, leetcode.com, and chatgpt.com. Solve one problem per day to unlock all sites until midnight!", + "description": "Your daily LeetCode companion! Solve leetcode problems of your chosen category. Solve one problem per day to unlock all sites until midnight!", "permissions": [ "storage", "declarativeNetRequest", @@ -50,7 +50,12 @@ "src/assets/data/blind75.json", "src/assets/data/neetcode150.json", "src/assets/data/neetcodeAll.json", - "src/assets/data/problemAliases.json" + "src/assets/data/problemAliases.json", + "src/content/api.js", + "src/content/detector.js", + "src/content/editor.js", + "src/content/ui.js", + "src/shared/constants.js" ], "matches": [ "" diff --git a/options.css b/options.css index 06e2289..ea20f58 100644 --- a/options.css +++ b/options.css @@ -525,3 +525,167 @@ input:focus + .slider { transform: scale(1.1); } +/* Exclusion List Styles */ +.exclusion-system-section { + margin-bottom: 24px; + padding: 20px; + background: #f0f9ff; + border: 1px solid #bae6fd; + border-radius: 8px; +} + +.exclusion-user-section { + margin-bottom: 20px; +} + +.exclusion-subtitle { + font-size: 16px; + font-weight: 600; + color: #1f2937; + margin-bottom: 8px; +} + +.exclusion-subtitle-description { + font-size: 13px; + color: #6b7280; + margin-bottom: 12px; + line-height: 1.5; +} + +.exclusion-list-system { + display: flex; + flex-direction: column; + gap: 8px; +} + +.exclusion-list-container { + display: flex; + flex-direction: column; + gap: 8px; + min-height: 40px; +} + +.exclusion-list-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + background: white; + border: 1px solid #e5e7eb; + border-radius: 8px; + transition: all 0.2s ease; +} + +.exclusion-list-item:hover { + border-color: #667eea; + box-shadow: 0 2px 4px rgba(102, 126, 234, 0.1); +} + +.exclusion-list-item-system { + background: #f9fafb; + border-color: #d1d5db; + cursor: default; +} + +.exclusion-list-item-system:hover { + border-color: #d1d5db; + box-shadow: none; +} + +.exclusion-domain { + font-size: 14px; + color: #1f2937; + font-weight: 500; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; +} + +.exclusion-lock-icon { + font-size: 14px; + opacity: 0.6; + margin-left: 8px; +} + +.exclusion-remove-button { + background: #fee2e2; + color: #991b1b; + border: 1px solid #fecaca; + padding: 6px 12px; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.exclusion-remove-button:hover { + background: #fecaca; + border-color: #fca5a5; + transform: translateY(-1px); +} + +.exclusion-remove-button:active { + transform: translateY(0); +} + +.exclusion-add-container { + display: flex; + gap: 12px; + margin-bottom: 12px; +} + +.exclusion-domain-input { + flex: 1; + padding: 12px 16px; + font-size: 14px; + border: 2px solid #e5e7eb; + border-radius: 8px; + background: white; + color: #1f2937; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + transition: border-color 0.2s ease; +} + +.exclusion-domain-input:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.exclusion-domain-input::placeholder { + color: #9ca3af; +} + +.exclusion-error { + padding: 10px 12px; + background: #fef2f2; + border: 1px solid #fecaca; + border-radius: 6px; + color: #991b1b; + font-size: 13px; + margin-bottom: 12px; + line-height: 1.4; +} + +.exclusion-counter { + font-size: 13px; + color: #6b7280; + text-align: right; + margin-top: 8px; +} + +.exclusion-counter span { + font-weight: 600; + color: #1f2937; +} + +.no-data { + padding: 16px; + text-align: center; + color: #9ca3af; + font-size: 14px; + font-style: italic; + background: #f9fafb; + border-radius: 8px; + border: 1px dashed #e5e7eb; +} + diff --git a/options.html b/options.html index ba77d99..1770687 100644 --- a/options.html +++ b/options.html @@ -86,6 +86,88 @@

Display Preferences

+ +
+
+
+ +

+ Clear the code editor content when you first open a problem each day. Subsequent refreshes will preserve your work. +

+
+ +
+
+ +
+
+
+ +

+ Select problems randomly from unsolved problems instead of going in sequence. +

+
+ +
+
+ + +
+

Exclusion List

+

+ Add up to 10 custom websites to exclude from redirection. System domains (LeetCode, NeetCode, Google OAuth) are always excluded to maintain extension functionality. +

+ +
+
+

System Domains (Required)

+

These domains are always excluded to maintain extension functionality.

+
+ +
+
+
+ +
+
+

Your Domains

+
+ +
Loading exclusion list...
+
+
+
+ +
+
+ + +
+ +
+ 0 / 10 domains +
+
+ +
+ +
diff --git a/options.js b/options.js index 2b00386..6f1f215 100644 --- a/options.js +++ b/options.js @@ -2,6 +2,19 @@ const ALIASES_PATH = "src/assets/data/problemAliases.json"; +// System-enforced domains (not user-editable) +const SYSTEM_EXCLUSION_LIST = [ + "leetcode.com", + "neetcode.io", + "accounts.google.com" +]; + +// Default user-editable exclusion list (examples) +const DEFAULT_USER_EXCLUSION_LIST = [ + "github.com", + "linkedin.com" +]; + const problemSetSelect = document.getElementById("problemSetSelect"); const totalProblems = document.getElementById("totalProblems"); const totalCategories = document.getElementById("totalCategories"); @@ -12,6 +25,14 @@ const confirmReset = document.getElementById("confirmReset"); const cancelReset = document.getElementById("cancelReset"); const celebrationToggle = document.getElementById("celebrationToggle"); const sortByDifficultyToggle = document.getElementById("sortByDifficultyToggle"); +const clearEditorOnFirstOpenToggle = document.getElementById("clearEditorOnFirstOpenToggle"); +const randomProblemSelectionToggle = document.getElementById("randomProblemSelectionToggle"); +const exclusionListContainer = document.getElementById("exclusionListContainer"); +const exclusionDomainInput = document.getElementById("exclusionDomainInput"); +const addExclusionButton = document.getElementById("addExclusionButton"); +const resetExclusionButton = document.getElementById("resetExclusionButton"); +const exclusionError = document.getElementById("exclusionError"); +const exclusionCount = document.getElementById("exclusionCount"); // Load aliases for NeetCode URL resolution let problemAliases = {}; @@ -86,7 +107,9 @@ async function loadSettings() { const result = await chrome.storage.sync.get([ "selectedProblemSet", "celebrationEnabled", - "sortByDifficulty" + "sortByDifficulty", + "clearEditorOnFirstOpen", + "randomProblemSelection" ]); const selectedSet = result.selectedProblemSet || "neetcode250"; problemSetSelect.value = selectedSet; @@ -98,6 +121,14 @@ async function loadSettings() { // Load sort by difficulty toggle setting (default: false) const sortByDifficulty = result.sortByDifficulty === true; sortByDifficultyToggle.checked = sortByDifficulty; + + // Load clear editor on first open toggle setting (default: false) + const clearEditorOnFirstOpen = result.clearEditorOnFirstOpen === true; + clearEditorOnFirstOpenToggle.checked = clearEditorOnFirstOpen; + + // Load random problem selection toggle setting (default: false) + const randomProblemSelection = result.randomProblemSelection === true; + randomProblemSelectionToggle.checked = randomProblemSelection; } } catch (error) { console.error("Failed to load settings:", error); @@ -194,6 +225,36 @@ sortByDifficultyToggle.addEventListener("change", async () => { } }); +// Handle clear editor on first open toggle +clearEditorOnFirstOpenToggle.addEventListener("change", async () => { + const enabled = clearEditorOnFirstOpenToggle.checked; + + try { + await chrome.storage.sync.set({ clearEditorOnFirstOpen: enabled }); + console.log("Clear editor on first open:", enabled ? "enabled" : "disabled"); + } catch (error) { + console.error("Failed to save clear editor on first open setting:", error); + } +}); + +// Handle random problem selection toggle +randomProblemSelectionToggle.addEventListener("change", async () => { + const enabled = randomProblemSelectionToggle.checked; + + try { + await chrome.storage.sync.set({ randomProblemSelection: enabled }); + console.log("Random problem selection:", enabled ? "enabled" : "disabled"); + + // Recompute next problem to reflect the new selection mode + await chrome.runtime.sendMessage({ type: "REFRESH_STATUS" }); + + // Reload settings to show updated current problem + await loadSettings(); + } catch (error) { + console.error("Failed to save random problem selection setting:", error); + } +}); + // Render category accordion async function renderCategoryAccordion() { const container = document.getElementById('categoryAccordion'); @@ -302,10 +363,270 @@ function createCategoryAccordionItem(category) { return div; } +// ============================================================================ +// EXCLUSION LIST MANAGEMENT +// ============================================================================ + +/** + * Validate domain format + * @param {string} domain - Domain to validate + * @returns {Object} { valid: boolean, error: string } + */ +function validateDomain(domain) { + if (!domain || typeof domain !== 'string') { + return { valid: false, error: 'Domain is required' }; + } + + const trimmed = domain.trim(); + + if (trimmed.length === 0) { + return { valid: false, error: 'Domain cannot be empty' }; + } + + // Basic domain validation regex + // Allows: subdomain.example.com, example.com, example.co.uk + // Does not allow: http://, https://, paths, etc. + const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-_.]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-_.]*[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/; + + if (!domainRegex.test(trimmed)) { + return { valid: false, error: 'Invalid domain format. Use format like: example.com' }; + } + + return { valid: true, error: null }; +} + +/** + * Load user exclusion list from storage + * @returns {Promise>} + */ +async function loadExclusionList() { + try { + const result = await chrome.storage.sync.get(['userExclusionList']); + let userExclusionList = result.userExclusionList; + + // If no exclusion list exists, initialize with defaults + if (!Array.isArray(userExclusionList) || userExclusionList.length === 0) { + userExclusionList = [...DEFAULT_USER_EXCLUSION_LIST]; + await saveExclusionList(userExclusionList); + } + + return userExclusionList; + } catch (error) { + console.error("Failed to load exclusion list:", error); + return [...DEFAULT_USER_EXCLUSION_LIST]; + } +} + +/** + * Save user exclusion list to storage + * @param {Array} userExclusionList - List of user-editable domains + * @returns {Promise} + */ +async function saveExclusionList(userExclusionList) { + try { + await chrome.storage.sync.set({ userExclusionList }); + console.log("User exclusion list saved:", userExclusionList); + + // Notify background script to update redirect rule + await chrome.runtime.sendMessage({ type: "REFRESH_STATUS" }); + } catch (error) { + console.error("Failed to save exclusion list:", error); + throw error; + } +} + +/** + * Render system exclusion list (read-only) + */ +function renderSystemExclusionList() { + const systemContainer = document.getElementById('systemExclusionList'); + systemContainer.innerHTML = ''; + + SYSTEM_EXCLUSION_LIST.forEach((domain) => { + const item = document.createElement('div'); + item.className = 'exclusion-list-item exclusion-list-item-system'; + + const domainSpan = document.createElement('span'); + domainSpan.className = 'exclusion-domain'; + domainSpan.textContent = domain; + + const lockIcon = document.createElement('span'); + lockIcon.className = 'exclusion-lock-icon'; + lockIcon.textContent = '🔒'; + lockIcon.title = 'System domain (required)'; + + item.appendChild(domainSpan); + item.appendChild(lockIcon); + systemContainer.appendChild(item); + }); +} + +/** + * Render user exclusion list UI + * @param {Array} userExclusionList - List of user-editable domains + */ +function renderExclusionList(userExclusionList) { + exclusionListContainer.innerHTML = ''; + + if (userExclusionList.length === 0) { + exclusionListContainer.innerHTML = '

No custom domains added. Add domains above to exclude them from redirection.

'; + exclusionCount.textContent = '0'; + return; + } + + userExclusionList.forEach((domain, index) => { + const item = document.createElement('div'); + item.className = 'exclusion-list-item'; + + const domainSpan = document.createElement('span'); + domainSpan.className = 'exclusion-domain'; + domainSpan.textContent = domain; + + const removeButton = document.createElement('button'); + removeButton.className = 'exclusion-remove-button'; + removeButton.textContent = 'Remove'; + removeButton.title = 'Remove this domain'; + removeButton.addEventListener('click', () => removeExclusionDomain(index)); + + item.appendChild(domainSpan); + item.appendChild(removeButton); + exclusionListContainer.appendChild(item); + }); + + exclusionCount.textContent = userExclusionList.length.toString(); +} + +/** + * Add domain to exclusion list + */ +async function addExclusionDomain() { + const domain = exclusionDomainInput.value.trim(); + + // Clear previous errors + exclusionError.style.display = 'none'; + exclusionError.textContent = ''; + + // Validate domain + const validation = validateDomain(domain); + if (!validation.valid) { + exclusionError.textContent = validation.error; + exclusionError.style.display = 'block'; + return; + } + + // Load current list + const exclusionList = await loadExclusionList(); + + // Check for duplicates (case-insensitive) - check both user list and system list + const domainLower = domain.toLowerCase(); + if (exclusionList.some(d => d.toLowerCase() === domainLower)) { + exclusionError.textContent = 'This domain is already in your exclusion list'; + exclusionError.style.display = 'block'; + return; + } + + // Check against system domains + if (SYSTEM_EXCLUSION_LIST.some(d => d.toLowerCase() === domainLower)) { + exclusionError.textContent = 'This domain is already excluded (system domain)'; + exclusionError.style.display = 'block'; + return; + } + + // Check max limit + if (exclusionList.length >= 10) { + exclusionError.textContent = 'Maximum of 10 domains allowed'; + exclusionError.style.display = 'block'; + return; + } + + // Add domain + exclusionList.push(domain); + await saveExclusionList(exclusionList); + + // Update UI + renderExclusionList(exclusionList); + exclusionDomainInput.value = ''; +} + +/** + * Remove domain from exclusion list + * @param {number} index - Index of domain to remove + */ +async function removeExclusionDomain(index) { + const exclusionList = await loadExclusionList(); + + if (index >= 0 && index < exclusionList.length) { + exclusionList.splice(index, 1); + await saveExclusionList(exclusionList); + renderExclusionList(exclusionList); + } +} + +/** + * Reset exclusion list to defaults + */ +async function resetExclusionList() { + if (!confirm('Reset to default domains? This will replace your current custom domains with: ' + DEFAULT_USER_EXCLUSION_LIST.join(', '))) { + return; + } + + const defaultList = [...DEFAULT_USER_EXCLUSION_LIST]; + await saveExclusionList(defaultList); + renderExclusionList(defaultList); + + // Clear any errors + exclusionError.style.display = 'none'; + exclusionError.textContent = ''; +} + +/** + * Initialize exclusion list UI + */ +async function initializeExclusionList() { + // Render system domains (read-only) + renderSystemExclusionList(); + + // Load and render user domains + const userExclusionList = await loadExclusionList(); + renderExclusionList(userExclusionList); + + // Add event listeners + addExclusionButton.addEventListener('click', addExclusionDomain); + resetExclusionButton.addEventListener('click', resetExclusionList); + + // Allow Enter key to add domain + exclusionDomainInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + addExclusionDomain(); + } + }); + + // Clear error on input + exclusionDomainInput.addEventListener('input', () => { + if (exclusionError.style.display === 'block') { + exclusionError.style.display = 'none'; + } + }); +} + // Initialize on load document.addEventListener("DOMContentLoaded", async () => { await loadAliases(); loadSettings(); renderCategoryAccordion(); + await initializeExclusionList(); + + // Listen for storage changes (e.g., when daily problem is solved) + chrome.storage.onChanged.addListener((changes, areaName) => { + if (areaName === 'local' && changes.dailySolveDate) { + // Daily problem solved, refresh the display to show updated progress + loadSettings(); + renderCategoryAccordion(); + } + if (areaName === 'sync' && changes.userExclusionList) { + // User exclusion list changed, refresh UI + loadExclusionList().then(renderExclusionList); + } + }); }); diff --git a/package-lock.json b/package-lock.json index a5168a6..5c9310e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,10 @@ "@testing-library/dom": "^9.3.4", "@types/chrome": "^0.0.268", "babel-jest": "^29.7.0", + "cheerio": "^1.2.0", "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0" + "jest-environment-jsdom": "^29.7.0", + "node-fetch": "^2.7.0" } }, "node_modules/@babel/code-frame": { @@ -2758,6 +2760,13 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2951,6 +2960,60 @@ "node": ">=10" } }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -3105,6 +3168,36 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -3303,6 +3396,47 @@ "dev": true, "license": "MIT" }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, "node_modules/domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -3317,6 +3451,37 @@ "node": ">=12" } }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3359,6 +3524,34 @@ "dev": true, "license": "MIT" }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -3919,6 +4112,39 @@ "dev": true, "license": "MIT" }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -5683,6 +5909,52 @@ "dev": true, "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -5720,6 +5992,19 @@ "node": ">=8" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nwsapi": { "version": "2.2.23", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", @@ -5901,6 +6186,33 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6692,6 +7004,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/undici": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.19.1.tgz", + "integrity": "sha512-Gpq0iNm5M6cQWlyHQv9MV+uOj1jWk7LpkoE5vSp/7zjb4zMdAcUD+VL5y0nH4p9EbUklq00eVIIX/XcDHzu5xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", diff --git a/package.json b/package.json index a2f14f1..a0d944a 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,12 @@ "devDependencies": { "@babel/core": "^7.23.0", "@babel/preset-env": "^7.23.0", + "@testing-library/dom": "^9.3.4", "@types/chrome": "^0.0.268", "babel-jest": "^29.7.0", + "cheerio": "^1.2.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "@testing-library/dom": "^9.3.4" + "node-fetch": "^2.7.0" } } - diff --git a/popup.js b/popup.js index 63f3948..40a3d3b 100644 --- a/popup.js +++ b/popup.js @@ -343,12 +343,16 @@ document.addEventListener("DOMContentLoaded", async () => { // Refresh status every 30 seconds setInterval(updateStatus, 30000); - // Listen for storage changes (e.g., when problem set changes) + // Listen for storage changes (e.g., when problem set changes or daily solve) chrome.storage.onChanged.addListener((changes, areaName) => { if (areaName === 'sync' && changes.selectedProblemSet) { // Problem set changed, refresh the display updateStatus(); } + if (areaName === 'local' && changes.dailySolveDate) { + // Daily problem solved, refresh the display to show updated progress + updateStatus(); + } }); }); diff --git a/src/assets/data/neetcodeAll.json b/src/assets/data/neetcodeAll.json index 829eee5..955b7d0 100644 --- a/src/assets/data/neetcodeAll.json +++ b/src/assets/data/neetcodeAll.json @@ -6,23 +6,35 @@ "name": "Arrays & Hashing", "problems": [ { - "slug": "contains-duplicate", - "leetcodeId": 217, - "title": "Contains Duplicate", + "slug": "two-sum", + "leetcodeId": 1, + "title": "Two Sum", "difficulty": "Easy" }, { - "slug": "valid-anagram", - "leetcodeId": 242, - "title": "Valid Anagram", + "slug": "longest-common-prefix", + "leetcodeId": 14, + "title": "Longest Common Prefix", "difficulty": "Easy" }, { - "slug": "two-sum", - "leetcodeId": 1, - "title": "Two Sum", + "slug": "find-the-index-of-the-first-occurrence-in-a-string", + "leetcodeId": 28, + "title": "Find The Index of The First Occurrence in a String", "difficulty": "Easy" }, + { + "slug": "valid-sudoku", + "leetcodeId": 36, + "title": "Valid Sudoku", + "difficulty": "Medium" + }, + { + "slug": "first-missing-positive", + "leetcodeId": 41, + "title": "First Missing Positive", + "difficulty": "Hard" + }, { "slug": "group-anagrams", "leetcodeId": 49, @@ -30,27 +42,33 @@ "difficulty": "Medium" }, { - "slug": "top-k-frequent-elements", - "leetcodeId": 347, - "title": "Top K Frequent Elements", - "difficulty": "Medium" + "slug": "length-of-last-word", + "leetcodeId": 58, + "title": "Length of Last Word", + "difficulty": "Easy" }, { - "slug": "product-of-array-except-self", - "leetcodeId": 238, - "title": "Product of Array Except Self", - "difficulty": "Medium" + "slug": "text-justification", + "leetcodeId": 68, + "title": "Text Justification", + "difficulty": "Hard" }, { - "slug": "valid-sudoku", - "leetcodeId": 36, - "title": "Valid Sudoku", + "slug": "sort-colors", + "leetcodeId": 75, + "title": "Sort Colors", "difficulty": "Medium" }, { - "slug": "encode-and-decode-strings", - "leetcodeId": 271, - "title": "Encode and Decode Strings", + "slug": "pascals-triangle", + "leetcodeId": 118, + "title": "Pascals Triangle", + "difficulty": "Easy" + }, + { + "slug": "best-time-to-buy-and-sell-stock-ii", + "leetcodeId": 122, + "title": "Best Time to Buy And Sell Stock II", "difficulty": "Medium" }, { @@ -60,47 +78,89 @@ "difficulty": "Medium" }, { - "slug": "find-all-numbers-disappeared-in-an-array", - "leetcodeId": 448, - "title": "Find All Numbers Disappeared in an Array", + "slug": "majority-element", + "leetcodeId": 169, + "title": "Majority Element", "difficulty": "Easy" }, { - "slug": "find-all-duplicates-in-an-array", - "leetcodeId": 442, - "title": "Find All Duplicates in an Array", + "slug": "largest-number", + "leetcodeId": 179, + "title": "Largest Number", "difficulty": "Medium" }, { - "slug": "first-missing-positive", - "leetcodeId": 41, - "title": "First Missing Positive", - "difficulty": "Hard" + "slug": "repeated-dna-sequences", + "leetcodeId": 187, + "title": "Repeated DNA Sequences", + "difficulty": "Medium" }, { - "slug": "design-hashset", - "leetcodeId": 705, - "title": "Design HashSet", + "slug": "isomorphic-strings", + "leetcodeId": 205, + "title": "Isomorphic Strings", "difficulty": "Easy" }, { - "slug": "design-hashmap", - "leetcodeId": 706, - "title": "Design HashMap", + "slug": "contains-duplicate", + "leetcodeId": 217, + "title": "Contains Duplicate", "difficulty": "Easy" }, { - "slug": "isomorphic-strings", - "leetcodeId": 205, - "title": "Isomorphic Strings", + "slug": "product-of-array-except-self", + "leetcodeId": 238, + "title": "Product of Array Except Self", + "difficulty": "Medium" + }, + { + "slug": "valid-anagram", + "leetcodeId": 242, + "title": "Valid Anagram", "difficulty": "Easy" }, + { + "slug": "encode-and-decode-strings", + "leetcodeId": 271, + "title": "Encode and Decode Strings", + "difficulty": "Medium" + }, + { + "slug": "wiggle-sort", + "leetcodeId": 280, + "title": "Wiggle Sort", + "difficulty": "Medium" + }, { "slug": "word-pattern", "leetcodeId": 290, "title": "Word Pattern", "difficulty": "Easy" }, + { + "slug": "range-sum-query-immutable", + "leetcodeId": 303, + "title": "Range Sum Query - Immutable", + "difficulty": "Easy" + }, + { + "slug": "range-sum-query-2d-immutable", + "leetcodeId": 304, + "title": "Range Sum Query 2D Immutable", + "difficulty": "Medium" + }, + { + "slug": "top-k-frequent-elements", + "leetcodeId": 347, + "title": "Top K Frequent Elements", + "difficulty": "Medium" + }, + { + "slug": "insert-delete-getrandom-o1", + "leetcodeId": 380, + "title": "Insert Delete Get Random O(1)", + "difficulty": "Medium" + }, { "slug": "ransom-note", "leetcodeId": 383, @@ -114,1518 +174,2970 @@ "difficulty": "Easy" }, { - "slug": "jewels-and-stones", - "leetcodeId": 771, - "title": "Jewels and Stones", - "difficulty": "Easy" - }, - { - "slug": "unique-number-of-occurrences", - "leetcodeId": 1207, - "title": "Unique Number of Occurrences", + "slug": "is-subsequence", + "leetcodeId": 392, + "title": "Is Subsequence", "difficulty": "Easy" }, { - "slug": "determine-if-two-strings-are-close", - "leetcodeId": 1657, - "title": "Determine if Two Strings Are Close", + "slug": "find-all-duplicates-in-an-array", + "leetcodeId": 442, + "title": "Find All Duplicates in an Array", "difficulty": "Medium" }, { - "slug": "equal-row-and-column-pairs", - "leetcodeId": 2352, - "title": "Equal Row and Column Pairs", - "difficulty": "Medium" - } - ] - }, - { - "name": "Two Pointers", - "problems": [ - { - "slug": "valid-palindrome", - "leetcodeId": 125, - "title": "Valid Palindrome", + "slug": "find-all-numbers-disappeared-in-an-array", + "leetcodeId": 448, + "title": "Find All Numbers Disappeared in an Array", "difficulty": "Easy" }, { - "slug": "two-sum-ii-input-array-is-sorted", - "leetcodeId": 167, - "title": "Two Sum II", + "slug": "continuous-subarray-sum", + "leetcodeId": 523, + "title": "Continuous Subarray Sum", "difficulty": "Medium" }, { - "slug": "3sum", - "leetcodeId": 15, - "title": "3Sum", + "slug": "encode-and-decode-tinyurl", + "leetcodeId": 535, + "title": "Encode and Decode TinyURL", "difficulty": "Medium" }, { - "slug": "container-with-most-water", - "leetcodeId": 11, - "title": "Container With Most Water", + "slug": "brick-wall", + "leetcodeId": 554, + "title": "Brick Wall", "difficulty": "Medium" }, { - "slug": "trapping-rain-water", - "leetcodeId": 42, - "title": "Trapping Rain Water", - "difficulty": "Hard" - }, - { - "slug": "3sum-closest", - "leetcodeId": 16, - "title": "3Sum Closest", + "slug": "subarray-sum-equals-k", + "leetcodeId": 560, + "title": "Subarray Sum Equals K", "difficulty": "Medium" }, { - "slug": "4sum", - "leetcodeId": 18, - "title": "4Sum", + "slug": "non-decreasing-array", + "leetcodeId": 665, + "title": "Non Decreasing Array", "difficulty": "Medium" }, { - "slug": "remove-duplicates-from-sorted-array", - "leetcodeId": 26, - "title": "Remove Duplicates from Sorted Array", + "slug": "design-hashset", + "leetcodeId": 705, + "title": "Design HashSet", "difficulty": "Easy" }, { - "slug": "remove-element", - "leetcodeId": 27, - "title": "Remove Element", + "slug": "design-hashmap", + "leetcodeId": 706, + "title": "Design HashMap", "difficulty": "Easy" }, { - "slug": "move-zeroes", - "leetcodeId": 283, - "title": "Move Zeroes", + "slug": "find-pivot-index", + "leetcodeId": 724, + "title": "Find Pivot Index", "difficulty": "Easy" }, { - "slug": "reverse-string", - "leetcodeId": 344, - "title": "Reverse String", + "slug": "jewels-and-stones", + "leetcodeId": 771, + "title": "Jewels and Stones", "difficulty": "Easy" }, { - "slug": "reverse-words-in-a-string", - "leetcodeId": 151, - "title": "Reverse Words in a String", + "slug": "push-dominoes", + "leetcodeId": 838, + "title": "Push Dominoes", "difficulty": "Medium" }, { - "slug": "squares-of-a-sorted-array", - "leetcodeId": 977, - "title": "Squares of a Sorted Array", + "slug": "sort-an-array", + "leetcodeId": 912, + "title": "Sort an Array", + "difficulty": "Medium" + }, + { + "slug": "unique-email-addresses", + "leetcodeId": 929, + "title": "Unique Email Addresses", "difficulty": "Easy" - } - ] - }, - { - "name": "Stack", - "problems": [ + }, { - "slug": "valid-parentheses", - "leetcodeId": 20, - "title": "Valid Parentheses", + "slug": "maximum-number-of-balloons", + "leetcodeId": 1189, + "title": "Maximum Number of Balloons", "difficulty": "Easy" }, { - "slug": "min-stack", - "leetcodeId": 155, - "title": "Min Stack", - "difficulty": "Medium" + "slug": "unique-number-of-occurrences", + "leetcodeId": 1207, + "title": "Unique Number of Occurrences", + "difficulty": "Easy" }, { - "slug": "evaluate-reverse-polish-notation", - "leetcodeId": 150, - "title": "Evaluate Reverse Polish Notation", - "difficulty": "Medium" + "slug": "replace-elements-with-greatest-element-on-right-side", + "leetcodeId": 1299, + "title": "Replace Elements With Greatest Element On Right Side", + "difficulty": "Easy" }, { - "slug": "generate-parentheses", - "leetcodeId": 22, - "title": "Generate Parentheses", + "slug": "design-underground-system", + "leetcodeId": 1396, + "title": "Design Underground System", "difficulty": "Medium" }, { - "slug": "daily-temperatures", - "leetcodeId": 739, - "title": "Daily Temperatures", + "slug": "check-if-a-string-contains-all-binary-codes-of-size-k", + "leetcodeId": 1461, + "title": "Check if a String Contains all Binary Codes of Size K", "difficulty": "Medium" }, { - "slug": "car-fleet", - "leetcodeId": 853, - "title": "Car Fleet", + "slug": "design-parking-system", + "leetcodeId": 1603, + "title": "Design Parking System", + "difficulty": "Easy" + }, + { + "slug": "determine-if-two-strings-are-close", + "leetcodeId": 1657, + "title": "Determine if Two Strings Are Close", "difficulty": "Medium" }, { - "slug": "largest-rectangle-in-histogram", - "leetcodeId": 84, - "title": "Largest Rectangle in Histogram", - "difficulty": "Hard" + "slug": "sign-of-the-product-of-an-array", + "leetcodeId": 1822, + "title": "Sign of An Array", + "difficulty": "Easy" }, { - "slug": "simplify-path", - "leetcodeId": 71, - "title": "Simplify Path", + "slug": "concatenation-of-array", + "leetcodeId": 1929, + "title": "Concatenation of Array", + "difficulty": "Easy" + }, + { + "slug": "unique-length-3-palindromic-subsequences", + "leetcodeId": 1930, + "title": "Unique Length 3 Palindromic Subsequences", "difficulty": "Medium" }, { - "slug": "decode-string", - "leetcodeId": 394, - "title": "Decode String", + "slug": "minimum-number-of-swaps-to-make-the-string-balanced", + "leetcodeId": 1963, + "title": "Minimum Number of Swaps to Make The String Balanced", "difficulty": "Medium" }, { - "slug": "asteroid-collision", - "leetcodeId": 735, - "title": "Asteroid Collision", + "slug": "number-of-pairs-of-interchangeable-rectangles", + "leetcodeId": 2001, + "title": "Number of Pairs of Interchangeable Rectangles", "difficulty": "Medium" }, { - "slug": "online-stock-span", - "leetcodeId": 901, - "title": "Online Stock Span", + "slug": "maximum-product-of-the-length-of-two-palindromic-subsequences", + "leetcodeId": 2002, + "title": "Maximum Product of The Length of Two Palindromic Subsequences", "difficulty": "Medium" }, { - "slug": "next-greater-element-i", - "leetcodeId": 496, - "title": "Next Greater Element I", + "slug": "grid-game", + "leetcodeId": 2017, + "title": "Grid Game", + "difficulty": "Medium" + }, + { + "slug": "find-the-difference-of-two-arrays", + "leetcodeId": 2215, + "title": "Find the Difference of Two Arrays", "difficulty": "Easy" }, { - "slug": "next-greater-element-ii", - "leetcodeId": 503, - "title": "Next Greater Element II", + "slug": "naming-a-company", + "leetcodeId": 2306, + "title": "Naming a Company", + "difficulty": "Hard" + }, + { + "slug": "number-of-zero-filled-subarrays", + "leetcodeId": 2348, + "title": "Number of Zero-Filled Subarrays", "difficulty": "Medium" }, { - "slug": "basic-calculator", + "slug": "equal-row-and-column-pairs", + "leetcodeId": 2352, + "title": "Equal Row and Column Pairs", + "difficulty": "Medium" + }, + { + "slug": "optimal-partition-of-string", + "leetcodeId": 2405, + "title": "Optimal Partition of String", + "difficulty": "Medium" + }, + { + "slug": "minimum-penalty-for-a-shop", + "leetcodeId": 2483, + "title": "Minimum Penalty for a Shop", + "difficulty": "Medium" + } + ] + }, + { + "name": "Two Pointers", + "problems": [ + { + "slug": "container-with-most-water", + "leetcodeId": 11, + "title": "Container With Most Water", + "difficulty": "Medium" + }, + { + "slug": "3sum", + "leetcodeId": 15, + "title": "3Sum", + "difficulty": "Medium" + }, + { + "slug": "3sum-closest", + "leetcodeId": 16, + "title": "3Sum Closest", + "difficulty": "Medium" + }, + { + "slug": "4sum", + "leetcodeId": 18, + "title": "4Sum", + "difficulty": "Medium" + }, + { + "slug": "remove-duplicates-from-sorted-array", + "leetcodeId": 26, + "title": "Remove Duplicates from Sorted Array", + "difficulty": "Easy" + }, + { + "slug": "remove-element", + "leetcodeId": 27, + "title": "Remove Element", + "difficulty": "Easy" + }, + { + "slug": "trapping-rain-water", + "leetcodeId": 42, + "title": "Trapping Rain Water", + "difficulty": "Hard" + }, + { + "slug": "remove-duplicates-from-sorted-array-ii", + "leetcodeId": 80, + "title": "Remove Duplicates From Sorted Array II", + "difficulty": "Medium" + }, + { + "slug": "merge-sorted-array", + "leetcodeId": 88, + "title": "Merge Sorted Array", + "difficulty": "Easy" + }, + { + "slug": "valid-palindrome", + "leetcodeId": 125, + "title": "Valid Palindrome", + "difficulty": "Easy" + }, + { + "slug": "reverse-words-in-a-string", + "leetcodeId": 151, + "title": "Reverse Words in a String", + "difficulty": "Medium" + }, + { + "slug": "two-sum-ii-input-array-is-sorted", + "leetcodeId": 167, + "title": "Two Sum II", + "difficulty": "Medium" + }, + { + "slug": "rotate-array", + "leetcodeId": 189, + "title": "Rotate Array", + "difficulty": "Medium" + }, + { + "slug": "move-zeroes", + "leetcodeId": 283, + "title": "Move Zeroes", + "difficulty": "Easy" + }, + { + "slug": "reverse-string", + "leetcodeId": 344, + "title": "Reverse String", + "difficulty": "Easy" + }, + { + "slug": "valid-palindrome-ii", + "leetcodeId": 680, + "title": "Valid Palindrome II", + "difficulty": "Easy" + }, + { + "slug": "boats-to-save-people", + "leetcodeId": 881, + "title": "Boats to Save People", + "difficulty": "Medium" + }, + { + "slug": "squares-of-a-sorted-array", + "leetcodeId": 977, + "title": "Squares of a Sorted Array", + "difficulty": "Easy" + }, + { + "slug": "number-of-subsequences-that-satisfy-the-given-sum-condition", + "leetcodeId": 1498, + "title": "Number of Subsequences That Satisfy The Given Sum Condition", + "difficulty": "Medium" + }, + { + "slug": "merge-strings-alternately", + "leetcodeId": 1768, + "title": "Merge Strings Alternately", + "difficulty": "Easy" + }, + { + "slug": "array-with-elements-not-equal-to-average-of-neighbors", + "leetcodeId": 1968, + "title": "Array With Elements Not Equal to Average of Neighbors", + "difficulty": "Medium" + }, + { + "slug": "minimum-difference-between-highest-and-lowest-of-k-scores", + "leetcodeId": 1984, + "title": "Minimum Difference Between Highest And Lowest of K Scores", + "difficulty": "Easy" + } + ] + }, + { + "name": "Stack", + "problems": [ + { + "slug": "valid-parentheses", + "leetcodeId": 20, + "title": "Valid Parentheses", + "difficulty": "Easy" + }, + { + "slug": "generate-parentheses", + "leetcodeId": 22, + "title": "Generate Parentheses", + "difficulty": "Medium" + }, + { + "slug": "simplify-path", + "leetcodeId": 71, + "title": "Simplify Path", + "difficulty": "Medium" + }, + { + "slug": "largest-rectangle-in-histogram", + "leetcodeId": 84, + "title": "Largest Rectangle in Histogram", + "difficulty": "Hard" + }, + { + "slug": "evaluate-reverse-polish-notation", + "leetcodeId": 150, + "title": "Evaluate Reverse Polish Notation", + "difficulty": "Medium" + }, + { + "slug": "min-stack", + "leetcodeId": 155, + "title": "Min Stack", + "difficulty": "Medium" + }, + { + "slug": "basic-calculator", "leetcodeId": 224, "title": "Basic Calculator", "difficulty": "Hard" + }, + { + "slug": "implement-stack-using-queues", + "leetcodeId": 225, + "title": "Implement Stack Using Queues", + "difficulty": "Easy" + }, + { + "slug": "decode-string", + "leetcodeId": 394, + "title": "Decode String", + "difficulty": "Medium" + }, + { + "slug": "remove-k-digits", + "leetcodeId": 402, + "title": "Remove K Digits", + "difficulty": "Medium" + }, + { + "slug": "132-pattern", + "leetcodeId": 456, + "title": "132 Pattern", + "difficulty": "Medium" + }, + { + "slug": "next-greater-element-i", + "leetcodeId": 496, + "title": "Next Greater Element I", + "difficulty": "Easy" + }, + { + "slug": "next-greater-element-ii", + "leetcodeId": 503, + "title": "Next Greater Element II", + "difficulty": "Medium" + }, + { + "slug": "baseball-game", + "leetcodeId": 682, + "title": "Baseball Game", + "difficulty": "Easy" + }, + { + "slug": "asteroid-collision", + "leetcodeId": 735, + "title": "Asteroid Collision", + "difficulty": "Medium" + }, + { + "slug": "daily-temperatures", + "leetcodeId": 739, + "title": "Daily Temperatures", + "difficulty": "Medium" + }, + { + "slug": "car-fleet", + "leetcodeId": 853, + "title": "Car Fleet", + "difficulty": "Medium" + }, + { + "slug": "online-stock-span", + "leetcodeId": 901, + "title": "Online Stock Span", + "difficulty": "Medium" + }, + { + "slug": "validate-stack-sequences", + "leetcodeId": 946, + "title": "Validate Stack Sequences", + "difficulty": "Medium" + }, + { + "slug": "remove-all-adjacent-duplicates-in-string-ii", + "leetcodeId": 1209, + "title": "Remove All Adjacent Duplicates In String II", + "difficulty": "Medium" + }, + { + "slug": "removing-stars-from-a-string", + "leetcodeId": 2390, + "title": "Removing Stars From a String", + "difficulty": "Medium" + } + ] + }, + { + "name": "Binary Search", + "problems": [ + { + "slug": "median-of-two-sorted-arrays", + "leetcodeId": 4, + "title": "Median of Two Sorted Arrays", + "difficulty": "Hard" + }, + { + "slug": "search-in-rotated-sorted-array", + "leetcodeId": 33, + "title": "Search in Rotated Sorted Array", + "difficulty": "Medium" + }, + { + "slug": "find-first-and-last-position-of-element-in-sorted-array", + "leetcodeId": 34, + "title": "Find First and Last Position of Element in Sorted Array", + "difficulty": "Medium" + }, + { + "slug": "search-insert-position", + "leetcodeId": 35, + "title": "Search Insert Position", + "difficulty": "Easy" + }, + { + "slug": "sqrtx", + "leetcodeId": 69, + "title": "Sqrt(x) ", + "difficulty": "Easy" + }, + { + "slug": "search-a-2d-matrix", + "leetcodeId": 74, + "title": "Search a 2D Matrix", + "difficulty": "Medium" + }, + { + "slug": "search-in-rotated-sorted-array-ii", + "leetcodeId": 81, + "title": "Search in Rotated Sorted Array II", + "difficulty": "Medium" + }, + { + "slug": "populating-next-right-pointers-in-each-node", + "leetcodeId": 116, + "title": "Populating Next Right Pointers In Each Node", + "difficulty": "Medium" + }, + { + "slug": "find-minimum-in-rotated-sorted-array", + "leetcodeId": 153, + "title": "Find Minimum in Rotated Sorted Array", + "difficulty": "Medium" + }, + { + "slug": "find-minimum-in-rotated-sorted-array-ii", + "leetcodeId": 154, + "title": "Find Minimum in Rotated Sorted Array II", + "difficulty": "Hard" + }, + { + "slug": "find-peak-element", + "leetcodeId": 162, + "title": "Find Peak Element", + "difficulty": "Medium" + }, + { + "slug": "search-a-2d-matrix-ii", + "leetcodeId": 240, + "title": "Search a 2D Matrix II", + "difficulty": "Medium" + }, + { + "slug": "valid-perfect-square", + "leetcodeId": 367, + "title": "Valid Perfect Square", + "difficulty": "Easy" + }, + { + "slug": "guess-number-higher-or-lower", + "leetcodeId": 374, + "title": "Guess Number Higher Or Lower", + "difficulty": "Easy" + }, + { + "slug": "split-array-largest-sum", + "leetcodeId": 410, + "title": "Split Array Largest Sum", + "difficulty": "Hard" + }, + { + "slug": "arranging-coins", + "leetcodeId": 441, + "title": "Arranging Coins", + "difficulty": "Easy" + }, + { + "slug": "single-element-in-a-sorted-array", + "leetcodeId": 540, + "title": "Single Element in a Sorted Array", + "difficulty": "Medium" + }, + { + "slug": "binary-search", + "leetcodeId": 704, + "title": "Binary Search", + "difficulty": "Easy" + }, + { + "slug": "koko-eating-bananas", + "leetcodeId": 875, + "title": "Koko Eating Bananas", + "difficulty": "Medium" + }, + { + "slug": "time-based-key-value-store", + "leetcodeId": 981, + "title": "Time Based Key-Value Store", + "difficulty": "Medium" + }, + { + "slug": "capacity-to-ship-packages-within-d-days", + "leetcodeId": 1011, + "title": "Capacity To Ship Packages Within D Days", + "difficulty": "Medium" + }, + { + "slug": "search-suggestions-system", + "leetcodeId": 1268, + "title": "Search Suggestions System", + "difficulty": "Medium" + }, + { + "slug": "maximum-number-of-removable-characters", + "leetcodeId": 1898, + "title": "Maximum Number of Removable Characters", + "difficulty": "Medium" + }, + { + "slug": "successful-pairs-of-spells-and-potions", + "leetcodeId": 2300, + "title": "Successful Pairs of Spells and Potions", + "difficulty": "Medium" + }, + { + "slug": "minimize-the-maximum-difference-of-pairs", + "leetcodeId": 2616, + "title": "Minimize the Maximum Difference of Pairs", + "difficulty": "Medium" + } + ] + }, + { + "name": "Sliding Window", + "problems": [ + { + "slug": "longest-substring-without-repeating-characters", + "leetcodeId": 3, + "title": "Longest Substring Without Repeating Characters", + "difficulty": "Medium" + }, + { + "slug": "minimum-window-substring", + "leetcodeId": 76, + "title": "Minimum Window Substring", + "difficulty": "Hard" + }, + { + "slug": "best-time-to-buy-and-sell-stock", + "leetcodeId": 121, + "title": "Best Time to Buy and Sell Stock", + "difficulty": "Easy" + }, + { + "slug": "minimum-size-subarray-sum", + "leetcodeId": 209, + "title": "Minimum Size Subarray Sum", + "difficulty": "Medium" + }, + { + "slug": "contains-duplicate-ii", + "leetcodeId": 219, + "title": "Contains Duplicate II", + "difficulty": "Easy" + }, + { + "slug": "sliding-window-maximum", + "leetcodeId": 239, + "title": "Sliding Window Maximum", + "difficulty": "Hard" + }, + { + "slug": "longest-repeating-character-replacement", + "leetcodeId": 424, + "title": "Longest Repeating Character Replacement", + "difficulty": "Medium" + }, + { + "slug": "find-all-anagrams-in-a-string", + "leetcodeId": 438, + "title": "Find All Anagrams in a String", + "difficulty": "Medium" + }, + { + "slug": "permutation-in-string", + "leetcodeId": 567, + "title": "Permutation in String", + "difficulty": "Medium" + }, + { + "slug": "find-k-closest-elements", + "leetcodeId": 658, + "title": "Find K Closest Elements", + "difficulty": "Medium" + }, + { + "slug": "fruit-into-baskets", + "leetcodeId": 904, + "title": "Fruit Into Baskets", + "difficulty": "Medium" + }, + { + "slug": "number-of-sub-arrays-of-size-k-and-average-greater-than-or-equal-to-threshold", + "leetcodeId": 1343, + "title": "Number of Sub Arrays of Size K and Avg Greater than or Equal to Threshold", + "difficulty": "Medium" + }, + { + "slug": "maximum-number-of-vowels-in-a-substring-of-given-length", + "leetcodeId": 1456, + "title": "Maximum Number of Vowels in a Substring of Given Length", + "difficulty": "Medium" + }, + { + "slug": "minimum-operations-to-reduce-x-to-zero", + "leetcodeId": 1658, + "title": "Minimum Operations to Reduce X to Zero", + "difficulty": "Medium" + }, + { + "slug": "frequency-of-the-most-frequent-element", + "leetcodeId": 1838, + "title": "Frequency of The Most Frequent Element", + "difficulty": "Medium" + }, + { + "slug": "minimum-number-of-flips-to-make-the-binary-string-alternating", + "leetcodeId": 1888, + "title": "Minimum Number of Flips to Make The Binary String Alternating", + "difficulty": "Medium" + } + ] + }, + { + "name": "Linked List", + "problems": [ + { + "slug": "add-two-numbers", + "leetcodeId": 2, + "title": "Add Two Numbers", + "difficulty": "Medium" + }, + { + "slug": "remove-nth-node-from-end-of-list", + "leetcodeId": 19, + "title": "Remove Nth Node From End of List", + "difficulty": "Medium" + }, + { + "slug": "merge-two-sorted-lists", + "leetcodeId": 21, + "title": "Merge Two Sorted Lists", + "difficulty": "Easy" + }, + { + "slug": "merge-k-sorted-lists", + "leetcodeId": 23, + "title": "Merge k Sorted Lists", + "difficulty": "Hard" + }, + { + "slug": "swap-nodes-in-pairs", + "leetcodeId": 24, + "title": "Swap Nodes in Pairs", + "difficulty": "Medium" + }, + { + "slug": "reverse-nodes-in-k-group", + "leetcodeId": 25, + "title": "Reverse Nodes in k-Group", + "difficulty": "Hard" + }, + { + "slug": "rotate-list", + "leetcodeId": 61, + "title": "Rotate List", + "difficulty": "Medium" + }, + { + "slug": "remove-duplicates-from-sorted-list", + "leetcodeId": 83, + "title": "Remove Duplicates From Sorted List", + "difficulty": "Easy" + }, + { + "slug": "partition-list", + "leetcodeId": 86, + "title": "Partition List", + "difficulty": "Medium" + }, + { + "slug": "reverse-linked-list-ii", + "leetcodeId": 92, + "title": "Reverse Linked List II", + "difficulty": "Medium" + }, + { + "slug": "copy-list-with-random-pointer", + "leetcodeId": 138, + "title": "Copy List with Random Pointer", + "difficulty": "Medium" + }, + { + "slug": "linked-list-cycle", + "leetcodeId": 141, + "title": "Linked List Cycle", + "difficulty": "Easy" + }, + { + "slug": "reorder-list", + "leetcodeId": 143, + "title": "Reorder List", + "difficulty": "Medium" + }, + { + "slug": "lru-cache", + "leetcodeId": 146, + "title": "LRU Cache", + "difficulty": "Medium" + }, + { + "slug": "insertion-sort-list", + "leetcodeId": 147, + "title": "Insertion Sort List", + "difficulty": "Medium" + }, + { + "slug": "sort-list", + "leetcodeId": 148, + "title": "Sort List", + "difficulty": "Medium" + }, + { + "slug": "intersection-of-two-linked-lists", + "leetcodeId": 160, + "title": "Intersection of Two Linked Lists", + "difficulty": "Easy" + }, + { + "slug": "remove-linked-list-elements", + "leetcodeId": 203, + "title": "Remove Linked List Elements", + "difficulty": "Easy" + }, + { + "slug": "reverse-linked-list", + "leetcodeId": 206, + "title": "Reverse Linked List", + "difficulty": "Easy" + }, + { + "slug": "palindrome-linked-list", + "leetcodeId": 234, + "title": "Palindrome Linked List", + "difficulty": "Easy" + }, + { + "slug": "find-the-duplicate-number", + "leetcodeId": 287, + "title": "Find the Duplicate Number", + "difficulty": "Medium" + }, + { + "slug": "odd-even-linked-list", + "leetcodeId": 328, + "title": "Odd Even Linked List", + "difficulty": "Medium" + }, + { + "slug": "lfu-cache", + "leetcodeId": 460, + "title": "LFU Cache", + "difficulty": "Hard" + }, + { + "slug": "design-circular-queue", + "leetcodeId": 622, + "title": "Design Circular Queue", + "difficulty": "Medium" + }, + { + "slug": "design-linked-list", + "leetcodeId": 707, + "title": "Design Linked List", + "difficulty": "Medium" + }, + { + "slug": "split-linked-list-in-parts", + "leetcodeId": 725, + "title": "Split Linked List in Parts", + "difficulty": "Medium" + }, + { + "slug": "middle-of-the-linked-list", + "leetcodeId": 876, + "title": "Middle of the Linked List", + "difficulty": "Easy" + }, + { + "slug": "design-browser-history", + "leetcodeId": 1472, + "title": "Design Browser History", + "difficulty": "Medium" + }, + { + "slug": "swapping-nodes-in-a-linked-list", + "leetcodeId": 1721, + "title": "Swapping Nodes in a Linked List", + "difficulty": "Medium" + }, + { + "slug": "maximum-twin-sum-of-a-linked-list", + "leetcodeId": 2130, + "title": "Maximum Twin Sum Of A Linked List", + "difficulty": "Medium" + } + ] + }, + { + "name": "Trees", + "problems": [ + { + "slug": "binary-tree-inorder-traversal", + "leetcodeId": 94, + "title": "Binary Tree Inorder Traversal", + "difficulty": "Easy" + }, + { + "slug": "unique-binary-search-trees-ii", + "leetcodeId": 95, + "title": "Unique Binary Search Trees II", + "difficulty": "Medium" + }, + { + "slug": "unique-binary-search-trees", + "leetcodeId": 96, + "title": "Unique Binary Search Trees", + "difficulty": "Medium" + }, + { + "slug": "validate-binary-search-tree", + "leetcodeId": 98, + "title": "Validate Binary Search Tree", + "difficulty": "Medium" + }, + { + "slug": "same-tree", + "leetcodeId": 100, + "title": "Same Tree", + "difficulty": "Easy" + }, + { + "slug": "symmetric-tree", + "leetcodeId": 101, + "title": "Symmetric Tree ", + "difficulty": "Easy" + }, + { + "slug": "binary-tree-level-order-traversal", + "leetcodeId": 102, + "title": "Binary Tree Level Order Traversal", + "difficulty": "Medium" + }, + { + "slug": "binary-tree-zigzag-level-order-traversal", + "leetcodeId": 103, + "title": "Binary Tree Zigzag Level Order Traversal", + "difficulty": "Medium" + }, + { + "slug": "maximum-depth-of-binary-tree", + "leetcodeId": 104, + "title": "Maximum Depth of Binary Tree", + "difficulty": "Easy" + }, + { + "slug": "construct-binary-tree-from-preorder-and-inorder-traversal", + "leetcodeId": 105, + "title": "Construct Binary Tree from Preorder and Inorder Traversal", + "difficulty": "Medium" + }, + { + "slug": "construct-binary-tree-from-inorder-and-postorder-traversal", + "leetcodeId": 106, + "title": "Construct Binary Tree from Inorder and Postorder Traversal", + "difficulty": "Medium" + }, + { + "slug": "convert-sorted-array-to-binary-search-tree", + "leetcodeId": 108, + "title": "Convert Sorted Array to Binary Search Tree", + "difficulty": "Easy" + }, + { + "slug": "balanced-binary-tree", + "leetcodeId": 110, + "title": "Balanced Binary Tree", + "difficulty": "Easy" + }, + { + "slug": "path-sum", + "leetcodeId": 112, + "title": "Path Sum", + "difficulty": "Easy" + }, + { + "slug": "path-sum-ii", + "leetcodeId": 113, + "title": "Path Sum II", + "difficulty": "Medium" + }, + { + "slug": "flatten-binary-tree-to-linked-list", + "leetcodeId": 114, + "title": "Flatten Binary Tree to Linked List", + "difficulty": "Medium" + }, + { + "slug": "binary-tree-maximum-path-sum", + "leetcodeId": 124, + "title": "Binary Tree Maximum Path Sum", + "difficulty": "Hard" + }, + { + "slug": "sum-root-to-leaf-numbers", + "leetcodeId": 129, + "title": "Sum Root to Leaf Numbers", + "difficulty": "Medium" + }, + { + "slug": "binary-tree-preorder-traversal", + "leetcodeId": 144, + "title": "Binary Tree Preorder Traversal", + "difficulty": "Easy" + }, + { + "slug": "binary-tree-postorder-traversal", + "leetcodeId": 145, + "title": "Binary Tree Postorder Traversal", + "difficulty": "Easy" + }, + { + "slug": "binary-search-tree-iterator", + "leetcodeId": 173, + "title": "Binary Search Tree Iterator", + "difficulty": "Medium" + }, + { + "slug": "binary-tree-right-side-view", + "leetcodeId": 199, + "title": "Binary Tree Right Side View", + "difficulty": "Medium" + }, + { + "slug": "invert-binary-tree", + "leetcodeId": 226, + "title": "Invert Binary Tree", + "difficulty": "Easy" + }, + { + "slug": "kth-smallest-element-in-a-bst", + "leetcodeId": 230, + "title": "Kth Smallest Element in a BST", + "difficulty": "Medium" + }, + { + "slug": "lowest-common-ancestor-of-a-binary-search-tree", + "leetcodeId": 235, + "title": "Lowest Common Ancestor of a BST", + "difficulty": "Medium" + }, + { + "slug": "lowest-common-ancestor-of-a-binary-tree", + "leetcodeId": 236, + "title": "Lowest Common Ancestor of a Binary Tree", + "difficulty": "Medium" + }, + { + "slug": "serialize-and-deserialize-binary-tree", + "leetcodeId": 297, + "title": "Serialize and Deserialize Binary Tree", + "difficulty": "Hard" + }, + { + "slug": "house-robber-iii", + "leetcodeId": 337, + "title": "House Robber III", + "difficulty": "Medium" + }, + { + "slug": "construct-quad-tree", + "leetcodeId": 427, + "title": "Construct Quad Tree", + "difficulty": "Medium" + }, + { + "slug": "path-sum-iii", + "leetcodeId": 437, + "title": "Path Sum III", + "difficulty": "Medium" + }, + { + "slug": "delete-node-in-a-bst", + "leetcodeId": 450, + "title": "Delete Node in a BST", + "difficulty": "Medium" + }, + { + "slug": "find-bottom-left-tree-value", + "leetcodeId": 513, + "title": "Find Bottom Left Tree Value", + "difficulty": "Medium" + }, + { + "slug": "convert-bst-to-greater-tree", + "leetcodeId": 538, + "title": "Convert Bst to Greater Tree", + "difficulty": "Medium" + }, + { + "slug": "diameter-of-binary-tree", + "leetcodeId": 543, + "title": "Diameter of Binary Tree", + "difficulty": "Easy" + }, + { + "slug": "subtree-of-another-tree", + "leetcodeId": 572, + "title": "Subtree of Another Tree", + "difficulty": "Easy" + }, + { + "slug": "construct-string-from-binary-tree", + "leetcodeId": 606, + "title": "Construct String From Binary Tree", + "difficulty": "Easy" + }, + { + "slug": "merge-two-binary-trees", + "leetcodeId": 617, + "title": "Merge Two Binary Trees", + "difficulty": "Easy" + }, + { + "slug": "find-duplicate-subtrees", + "leetcodeId": 652, + "title": "Find Duplicate Subtrees", + "difficulty": "Medium" + }, + { + "slug": "maximum-width-of-binary-tree", + "leetcodeId": 662, + "title": "Maximum Width of Binary Tree ", + "difficulty": "Medium" + }, + { + "slug": "trim-a-binary-search-tree", + "leetcodeId": 669, + "title": "Trim a Binary Search Tree", + "difficulty": "Medium" + }, + { + "slug": "insert-into-a-binary-search-tree", + "leetcodeId": 701, + "title": "Insert into a Binary Search Tree", + "difficulty": "Medium" + }, + { + "slug": "minimum-distance-between-bst-nodes", + "leetcodeId": 783, + "title": "Minimum Distance between BST Nodes", + "difficulty": "Easy" + }, + { + "slug": "all-possible-full-binary-trees", + "leetcodeId": 894, + "title": "All Possible Full Binary Trees", + "difficulty": "Medium" + }, + { + "slug": "flip-equivalent-binary-trees", + "leetcodeId": 951, + "title": "Flip Equivalent Binary Trees", + "difficulty": "Medium" + }, + { + "slug": "check-completeness-of-a-binary-tree", + "leetcodeId": 958, + "title": "Check Completeness of a Binary Tree", + "difficulty": "Medium" + }, + { + "slug": "time-needed-to-inform-all-employees", + "leetcodeId": 1376, + "title": "Time Needed to Inform All Employees ", + "difficulty": "Medium" + }, + { + "slug": "minimum-time-to-collect-all-apples-in-a-tree", + "leetcodeId": 1443, + "title": "Minimum Time to Collect All Apples in a Tree", + "difficulty": "Medium" + }, + { + "slug": "count-good-nodes-in-binary-tree", + "leetcodeId": 1448, + "title": "Count Good Nodes in Binary Tree", + "difficulty": "Medium" + }, + { + "slug": "operations-on-tree", + "leetcodeId": 1993, + "title": "Operations On Tree", + "difficulty": "Medium" + } + ] + }, + { + "name": "Tries", + "problems": [ + { + "slug": "implement-trie-prefix-tree", + "leetcodeId": 208, + "title": "Implement Trie (Prefix Tree)", + "difficulty": "Medium" + }, + { + "slug": "design-add-and-search-words-data-structure", + "leetcodeId": 211, + "title": "Design Add and Search Words Data Structure", + "difficulty": "Medium" + }, + { + "slug": "word-search-ii", + "leetcodeId": 212, + "title": "Word Search II", + "difficulty": "Hard" + }, + { + "slug": "prefix-and-suffix-search", + "leetcodeId": 745, + "title": "Prefix and Suffix Search", + "difficulty": "Hard" + }, + { + "slug": "extra-characters-in-a-string", + "leetcodeId": 2707, + "title": "Extra Characters in a String", + "difficulty": "Medium" + } + ] + }, + { + "name": "Heaps / Priority Queues", + "problems": [ + { + "slug": "kth-largest-element-in-an-array", + "leetcodeId": 215, + "title": "Kth Largest Element in an Array", + "difficulty": "Medium" + }, + { + "slug": "find-median-from-data-stream", + "leetcodeId": 295, + "title": "Find Median from Data Stream", + "difficulty": "Hard" + }, + { + "slug": "design-twitter", + "leetcodeId": 355, + "title": "Design Twitter", + "difficulty": "Medium" + }, + { + "slug": "kth-smallest-element-in-a-sorted-matrix", + "leetcodeId": 378, + "title": "Kth Smallest Element in a Sorted Matrix", + "difficulty": "Medium" + }, + { + "slug": "task-scheduler", + "leetcodeId": 621, + "title": "Task Scheduler", + "difficulty": "Medium" + }, + { + "slug": "kth-largest-element-in-a-stream", + "leetcodeId": 703, + "title": "Kth Largest Element in a Stream", + "difficulty": "Easy" + }, + { + "slug": "reorganize-string", + "leetcodeId": 767, + "title": "Reorganize String", + "difficulty": "Medium" + }, + { + "slug": "maximum-frequency-stack", + "leetcodeId": 895, + "title": "Maximum Frequency Stack", + "difficulty": "Hard" + }, + { + "slug": "k-closest-points-to-origin", + "leetcodeId": 973, + "title": "K Closest Points to Origin", + "difficulty": "Medium" + }, + { + "slug": "last-stone-weight", + "leetcodeId": 1046, + "title": "Last Stone Weight", + "difficulty": "Easy" + }, + { + "slug": "furthest-building-you-can-reach", + "leetcodeId": 1642, + "title": "Furthest Building You Can Reach", + "difficulty": "Medium" + }, + { + "slug": "smallest-number-in-infinite-set", + "leetcodeId": 2336, + "title": "Smallest Number in Infinite Set", + "difficulty": "Medium" + }, + { + "slug": "ipo", + "leetcodeId": 502, + "title": "IPO", + "difficulty": "Hard" + }, + { + "slug": "car-pooling", + "leetcodeId": 1094, + "title": "Car Pooling", + "difficulty": "Medium" + }, + { + "slug": "maximum-performance-of-a-team", + "leetcodeId": 1383, + "title": "Maximum Performance of a Team", + "difficulty": "Hard" + }, + { + "slug": "longest-happy-string", + "leetcodeId": 1405, + "title": "Longest Happy String", + "difficulty": "Medium" + }, + { + "slug": "minimize-deviation-in-array", + "leetcodeId": 1675, + "title": "Minimize Deviation in Array", + "difficulty": "Hard" + }, + { + "slug": "single-threaded-cpu", + "leetcodeId": 1834, + "title": "Single Threaded Cpu", + "difficulty": "Medium" + }, + { + "slug": "seat-reservation-manager", + "leetcodeId": 1845, + "title": "Seat Reservation Manager", + "difficulty": "Medium" + }, + { + "slug": "process-tasks-using-servers", + "leetcodeId": 1882, + "title": "Process Tasks Using Servers", + "difficulty": "Medium" + }, + { + "slug": "find-the-kth-largest-integer-in-the-array", + "leetcodeId": 1985, + "title": "Find The Kth Largest Integer In The Array", + "difficulty": "Medium" + }, + { + "slug": "maximum-subsequence-score", + "leetcodeId": 2542, + "title": "Maximum Subsequence Score", + "difficulty": "Medium" + } + ] + }, + { + "name": "Backtracking", + "problems": [ + { + "slug": "letter-combinations-of-a-phone-number", + "leetcodeId": 17, + "title": "Letter Combinations of a Phone Number", + "difficulty": "Medium" + }, + { + "slug": "sudoku-solver", + "leetcodeId": 37, + "title": "Sudoku Solver", + "difficulty": "Hard" + }, + { + "slug": "combination-sum", + "leetcodeId": 39, + "title": "Combination Sum", + "difficulty": "Medium" + }, + { + "slug": "combination-sum-ii", + "leetcodeId": 40, + "title": "Combination Sum II", + "difficulty": "Medium" + }, + { + "slug": "permutations", + "leetcodeId": 46, + "title": "Permutations", + "difficulty": "Medium" + }, + { + "slug": "permutations-ii", + "leetcodeId": 47, + "title": "Permutations II", + "difficulty": "Medium" + }, + { + "slug": "n-queens", + "leetcodeId": 51, + "title": "N-Queens", + "difficulty": "Hard" + }, + { + "slug": "n-queens-ii", + "leetcodeId": 52, + "title": "N-Queens II", + "difficulty": "Hard" + }, + { + "slug": "combinations", + "leetcodeId": 77, + "title": "Combinations", + "difficulty": "Medium" + }, + { + "slug": "subsets", + "leetcodeId": 78, + "title": "Subsets", + "difficulty": "Medium" + }, + { + "slug": "word-search", + "leetcodeId": 79, + "title": "Word Search", + "difficulty": "Medium" + }, + { + "slug": "subsets-ii", + "leetcodeId": 90, + "title": "Subsets II", + "difficulty": "Medium" + }, + { + "slug": "restore-ip-addresses", + "leetcodeId": 93, + "title": "Restore IP Addresses", + "difficulty": "Medium" + }, + { + "slug": "palindrome-partitioning", + "leetcodeId": 131, + "title": "Palindrome Partitioning", + "difficulty": "Medium" + }, + { + "slug": "matchsticks-to-square", + "leetcodeId": 473, + "title": "Matchsticks to Square", + "difficulty": "Medium" + }, + { + "slug": "beautiful-arrangement", + "leetcodeId": 526, + "title": "Beautiful Arrangement", + "difficulty": "Medium" + }, + { + "slug": "partition-to-k-equal-sum-subsets", + "leetcodeId": 698, + "title": "Partition to K Equal Sum Subsets", + "difficulty": "Medium" + }, + { + "slug": "maximum-length-of-a-concatenated-string-with-unique-characters", + "leetcodeId": 1239, + "title": "Maximum Length of a Concatenated String With Unique Characters", + "difficulty": "Medium" + }, + { + "slug": "splitting-a-string-into-descending-consecutive-values", + "leetcodeId": 1849, + "title": "Splitting a String Into Descending Consecutive Values", + "difficulty": "Medium" + }, + { + "slug": "find-unique-binary-string", + "leetcodeId": 1980, + "title": "Find Unique Binary String", + "difficulty": "Medium" } ] }, { - "name": "Binary Search", + "name": "Graphs", "problems": [ { - "slug": "binary-search", - "leetcodeId": 704, - "title": "Binary Search", + "slug": "word-ladder", + "leetcodeId": 127, + "title": "Word Ladder", + "difficulty": "Hard" + }, + { + "slug": "surrounded-regions", + "leetcodeId": 130, + "title": "Surrounded Regions", + "difficulty": "Medium" + }, + { + "slug": "clone-graph", + "leetcodeId": 133, + "title": "Clone Graph", + "difficulty": "Medium" + }, + { + "slug": "number-of-islands", + "leetcodeId": 200, + "title": "Number of Islands", + "difficulty": "Medium" + }, + { + "slug": "course-schedule", + "leetcodeId": 207, + "title": "Course Schedule", + "difficulty": "Medium" + }, + { + "slug": "course-schedule-ii", + "leetcodeId": 210, + "title": "Course Schedule II", + "difficulty": "Medium" + }, + { + "slug": "graph-valid-tree", + "leetcodeId": 261, + "title": "Graph Valid Tree", + "difficulty": "Medium" + }, + { + "slug": "walls-and-gates", + "leetcodeId": 286, + "title": "Walls and Gates", + "difficulty": "Medium" + }, + { + "slug": "number-of-connected-components-in-an-undirected-graph", + "leetcodeId": 323, + "title": "Number of Connected Components in an Undirected Graph", + "difficulty": "Medium" + }, + { + "slug": "evaluate-division", + "leetcodeId": 399, + "title": "Evaluate Division", + "difficulty": "Medium" + }, + { + "slug": "pacific-atlantic-water-flow", + "leetcodeId": 417, + "title": "Pacific Atlantic Water Flow", + "difficulty": "Medium" + }, + { + "slug": "island-perimeter", + "leetcodeId": 463, + "title": "Island Perimeter", "difficulty": "Easy" }, { - "slug": "search-a-2d-matrix", - "leetcodeId": 74, - "title": "Search a 2D Matrix", + "slug": "01-matrix", + "leetcodeId": 542, + "title": "01 Matrix", "difficulty": "Medium" }, { - "slug": "koko-eating-bananas", - "leetcodeId": 875, - "title": "Koko Eating Bananas", + "slug": "redundant-connection", + "leetcodeId": 684, + "title": "Redundant Connection", "difficulty": "Medium" }, { - "slug": "find-minimum-in-rotated-sorted-array", - "leetcodeId": 153, - "title": "Find Minimum in Rotated Sorted Array", + "slug": "max-area-of-island", + "leetcodeId": 695, + "title": "Max Area of Island", "difficulty": "Medium" }, { - "slug": "search-in-rotated-sorted-array", - "leetcodeId": 33, - "title": "Search in Rotated Sorted Array", + "slug": "accounts-merge", + "leetcodeId": 721, + "title": "Accounts Merge", "difficulty": "Medium" }, { - "slug": "time-based-key-value-store", - "leetcodeId": 981, - "title": "Time Based Key-Value Store", + "slug": "open-the-lock", + "leetcodeId": 752, + "title": "Open The Lock", "difficulty": "Medium" }, { - "slug": "median-of-two-sorted-arrays", - "leetcodeId": 4, - "title": "Median of Two Sorted Arrays", - "difficulty": "Hard" + "slug": "is-graph-bipartite", + "leetcodeId": 785, + "title": "Is Graph Bipartite?", + "difficulty": "Medium" }, { - "slug": "search-a-2d-matrix-ii", - "leetcodeId": 240, - "title": "Search a 2D Matrix II", + "slug": "find-eventual-safe-states", + "leetcodeId": 802, + "title": "Find Eventual Safe States", "difficulty": "Medium" }, { - "slug": "find-first-and-last-position-of-element-in-sorted-array", - "leetcodeId": 34, - "title": "Find First and Last Position of Element in Sorted Array", + "slug": "keys-and-rooms", + "leetcodeId": 841, + "title": "Keys and Rooms", "difficulty": "Medium" }, { - "slug": "find-peak-element", - "leetcodeId": 162, - "title": "Find Peak Element", + "slug": "snakes-and-ladders", + "leetcodeId": 909, + "title": "Snakes And Ladders", "difficulty": "Medium" }, { - "slug": "search-insert-position", - "leetcodeId": 35, - "title": "Search Insert Position", + "slug": "shortest-bridge", + "leetcodeId": 934, + "title": "Shortest Bridge", + "difficulty": "Medium" + }, + { + "slug": "verifying-an-alien-dictionary", + "leetcodeId": 953, + "title": "Verifying An Alien Dictionary", "difficulty": "Easy" }, { - "slug": "find-minimum-in-rotated-sorted-array-ii", - "leetcodeId": 154, - "title": "Find Minimum in Rotated Sorted Array II", + "slug": "rotting-oranges", + "leetcodeId": 994, + "title": "Rotting Oranges", + "difficulty": "Medium" + }, + { + "slug": "find-the-town-judge", + "leetcodeId": 997, + "title": "Find the Town Judge", + "difficulty": "Easy" + }, + { + "slug": "number-of-enclaves", + "leetcodeId": 1020, + "title": "Number of Enclaves", + "difficulty": "Medium" + }, + { + "slug": "shortest-path-in-binary-matrix", + "leetcodeId": 1091, + "title": "Shortest Path in Binary Matrix", + "difficulty": "Medium" + }, + { + "slug": "shortest-path-with-alternating-colors", + "leetcodeId": 1129, + "title": "Shortest Path with Alternating Colors", + "difficulty": "Medium" + }, + { + "slug": "as-far-from-land-as-possible", + "leetcodeId": 1162, + "title": "As Far from Land as Possible", + "difficulty": "Medium" + }, + { + "slug": "number-of-closed-islands", + "leetcodeId": 1254, + "title": "Number of Closed Islands", + "difficulty": "Medium" + }, + { + "slug": "course-schedule-iv", + "leetcodeId": 1462, + "title": "Course Schedule IV", + "difficulty": "Medium" + }, + { + "slug": "reorder-routes-to-make-all-paths-lead-to-the-city-zero", + "leetcodeId": 1466, + "title": "Reorder Routes to Make All Paths Lead to The City Zero", + "difficulty": "Medium" + }, + { + "slug": "minimum-number-of-days-to-eat-n-oranges", + "leetcodeId": 1553, + "title": "Minimum Number of Days to Eat N Oranges", "difficulty": "Hard" }, { - "slug": "search-in-rotated-sorted-array-ii", - "leetcodeId": 81, - "title": "Search in Rotated Sorted Array II", + "slug": "minimum-number-of-vertices-to-reach-all-nodes", + "leetcodeId": 1557, + "title": "Minimum Number of Vertices to Reach all Nodes", "difficulty": "Medium" }, { - "slug": "capacity-to-ship-packages-within-d-days", - "leetcodeId": 1011, - "title": "Capacity To Ship Packages Within D Days", + "slug": "largest-color-value-in-a-directed-graph", + "leetcodeId": 1857, + "title": "Largest Color Value in a Directed Graph", + "difficulty": "Hard" + }, + { + "slug": "count-sub-islands", + "leetcodeId": 1905, + "title": "Count Sub Islands", + "difficulty": "Medium" + }, + { + "slug": "check-if-move-is-legal", + "leetcodeId": 1958, + "title": "Check if Move Is Legal", + "difficulty": "Medium" + }, + { + "slug": "detonate-the-maximum-bombs", + "leetcodeId": 2101, + "title": "Detonate the Maximum Bombs", + "difficulty": "Medium" + }, + { + "slug": "find-closest-node-to-given-two-nodes", + "leetcodeId": 2359, + "title": "Find Closest Node to Given Two Nodes", + "difficulty": "Medium" + }, + { + "slug": "minimum-fuel-cost-to-report-to-the-capital", + "leetcodeId": 2477, + "title": "Minimum Fuel Cost to Report to the Capital", + "difficulty": "Medium" + }, + { + "slug": "minimum-score-of-a-path-between-two-cities", + "leetcodeId": 2492, + "title": "Minimum Score of a Path Between Two Cities", "difficulty": "Medium" } ] }, { - "name": "Sliding Window", + "name": "Advanced Graphs", "problems": [ { - "slug": "best-time-to-buy-and-sell-stock", - "leetcodeId": 121, - "title": "Best Time to Buy and Sell Stock", - "difficulty": "Easy" + "slug": "alien-dictionary", + "leetcodeId": 269, + "title": "Alien Dictionary", + "difficulty": "Hard" }, { - "slug": "longest-substring-without-repeating-characters", - "leetcodeId": 3, - "title": "Longest Substring Without Repeating Characters", + "slug": "reconstruct-itinerary", + "leetcodeId": 332, + "title": "Reconstruct Itinerary", + "difficulty": "Hard" + }, + { + "slug": "network-delay-time", + "leetcodeId": 743, + "title": "Network Delay Time", "difficulty": "Medium" }, { - "slug": "longest-repeating-character-replacement", - "leetcodeId": 424, - "title": "Longest Repeating Character Replacement", + "slug": "swim-in-rising-water", + "leetcodeId": 778, + "title": "Swim in Rising Water", + "difficulty": "Hard" + }, + { + "slug": "cheapest-flights-within-k-stops", + "leetcodeId": 787, + "title": "Cheapest Flights Within K Stops", "difficulty": "Medium" }, { - "slug": "permutation-in-string", - "leetcodeId": 567, - "title": "Permutation in String", + "slug": "critical-connections-in-a-network", + "leetcodeId": 1192, + "title": "Critical Connections in a Network", + "difficulty": "Hard" + }, + { + "slug": "find-the-city-with-the-smallest-number-of-neighbors-at-a-threshold-distance", + "leetcodeId": 1334, + "title": "Find the City With the Smallest Number of Neighbors at a Threshold Distance", "difficulty": "Medium" }, { - "slug": "minimum-window-substring", - "leetcodeId": 76, - "title": "Minimum Window Substring", + "slug": "find-critical-and-pseudo-critical-edges-in-minimum-spanning-tree", + "leetcodeId": 1489, + "title": "Find Critical and Pseudo Critical Edges in Minimum Spanning Tree", "difficulty": "Hard" }, { - "slug": "sliding-window-maximum", - "leetcodeId": 239, - "title": "Sliding Window Maximum", + "slug": "path-with-maximum-probability", + "leetcodeId": 1514, + "title": "Path with Maximum Probability", + "difficulty": "Medium" + }, + { + "slug": "remove-max-number-of-edges-to-keep-graph-fully-traversable", + "leetcodeId": 1579, + "title": "Remove Max Number of Edges to Keep Graph Fully Traversable", "difficulty": "Hard" }, { - "slug": "find-all-anagrams-in-a-string", - "leetcodeId": 438, - "title": "Find All Anagrams in a String", + "slug": "min-cost-to-connect-all-points", + "leetcodeId": 1584, + "title": "Min Cost to Connect All Points", "difficulty": "Medium" }, { - "slug": "minimum-size-subarray-sum", - "leetcodeId": 209, - "title": "Minimum Size Subarray Sum", + "slug": "path-with-minimum-effort", + "leetcodeId": 1631, + "title": "Path With Minimum Effort", "difficulty": "Medium" }, { - "slug": "fruit-into-baskets", - "leetcodeId": 904, - "title": "Fruit Into Baskets", - "difficulty": "Medium" + "slug": "parallel-courses-iii", + "leetcodeId": 2050, + "title": "Parallel Courses III", + "difficulty": "Hard" + }, + { + "slug": "number-of-good-paths", + "leetcodeId": 2421, + "title": "Number of Good Paths", + "difficulty": "Hard" } ] }, { - "name": "Linked List", + "name": "1-D Dynamic Programming", "problems": [ { - "slug": "reverse-linked-list", - "leetcodeId": 206, - "title": "Reverse Linked List", - "difficulty": "Easy" + "slug": "longest-palindromic-substring", + "leetcodeId": 5, + "title": "Longest Palindromic Substring", + "difficulty": "Medium" }, { - "slug": "merge-two-sorted-lists", - "leetcodeId": 21, - "title": "Merge Two Sorted Lists", + "slug": "climbing-stairs", + "leetcodeId": 70, + "title": "Climbing Stairs", "difficulty": "Easy" }, { - "slug": "reorder-list", - "leetcodeId": 143, - "title": "Reorder List", + "slug": "decode-ways", + "leetcodeId": 91, + "title": "Decode Ways", "difficulty": "Medium" }, { - "slug": "remove-nth-node-from-end-of-list", - "leetcodeId": 19, - "title": "Remove Nth Node From End of List", + "slug": "word-break", + "leetcodeId": 139, + "title": "Word Break", "difficulty": "Medium" }, { - "slug": "copy-list-with-random-pointer", - "leetcodeId": 138, - "title": "Copy List with Random Pointer", + "slug": "maximum-product-subarray", + "leetcodeId": 152, + "title": "Maximum Product Subarray", "difficulty": "Medium" }, { - "slug": "add-two-numbers", - "leetcodeId": 2, - "title": "Add Two Numbers", + "slug": "house-robber", + "leetcodeId": 198, + "title": "House Robber", "difficulty": "Medium" }, { - "slug": "linked-list-cycle", - "leetcodeId": 141, - "title": "Linked List Cycle", - "difficulty": "Easy" + "slug": "house-robber-ii", + "leetcodeId": 213, + "title": "House Robber II", + "difficulty": "Medium" }, { - "slug": "find-the-duplicate-number", - "leetcodeId": 287, - "title": "Find the Duplicate Number", + "slug": "paint-house", + "leetcodeId": 256, + "title": "Paint House", "difficulty": "Medium" }, { - "slug": "lru-cache", - "leetcodeId": 146, - "title": "LRU Cache", + "slug": "perfect-squares", + "leetcodeId": 279, + "title": "Perfect Squares", "difficulty": "Medium" }, { - "slug": "merge-k-sorted-lists", - "leetcodeId": 23, - "title": "Merge k Sorted Lists", - "difficulty": "Hard" + "slug": "longest-increasing-subsequence", + "leetcodeId": 300, + "title": "Longest Increasing Subsequence", + "difficulty": "Medium" }, { - "slug": "reverse-nodes-in-k-group", - "leetcodeId": 25, - "title": "Reverse Nodes in k-Group", - "difficulty": "Hard" + "slug": "coin-change", + "leetcodeId": 322, + "title": "Coin Change", + "difficulty": "Medium" }, { - "slug": "remove-linked-list-elements", - "leetcodeId": 203, - "title": "Remove Linked List Elements", - "difficulty": "Easy" + "slug": "integer-break", + "leetcodeId": 343, + "title": "Integer Break", + "difficulty": "Medium" }, { - "slug": "odd-even-linked-list", - "leetcodeId": 328, - "title": "Odd Even Linked List", + "slug": "combination-sum-iv", + "leetcodeId": 377, + "title": "Combination Sum IV", "difficulty": "Medium" }, { - "slug": "swap-nodes-in-pairs", - "leetcodeId": 24, - "title": "Swap Nodes in Pairs", + "slug": "partition-equal-subset-sum", + "leetcodeId": 416, + "title": "Partition Equal Subset Sum", "difficulty": "Medium" - } - ] - }, - { - "name": "Trees", - "problems": [ - { - "slug": "invert-binary-tree", - "leetcodeId": 226, - "title": "Invert Binary Tree", - "difficulty": "Easy" }, { - "slug": "maximum-depth-of-binary-tree", - "leetcodeId": 104, - "title": "Maximum Depth of Binary Tree", - "difficulty": "Easy" + "slug": "concatenated-words", + "leetcodeId": 472, + "title": "Concatenated Words", + "difficulty": "Hard" }, { - "slug": "diameter-of-binary-tree", - "leetcodeId": 543, - "title": "Diameter of Binary Tree", + "slug": "fibonacci-number", + "leetcodeId": 509, + "title": "Fibonacci Number", "difficulty": "Easy" }, { - "slug": "balanced-binary-tree", - "leetcodeId": 110, - "title": "Balanced Binary Tree", - "difficulty": "Easy" + "slug": "palindromic-substrings", + "leetcodeId": 647, + "title": "Palindromic Substrings", + "difficulty": "Medium" }, { - "slug": "same-tree", - "leetcodeId": 100, - "title": "Same Tree", - "difficulty": "Easy" + "slug": "number-of-longest-increasing-subsequence", + "leetcodeId": 673, + "title": "Number of Longest Increasing Subsequence", + "difficulty": "Medium" }, { - "slug": "subtree-of-another-tree", - "leetcodeId": 572, - "title": "Subtree of Another Tree", - "difficulty": "Easy" + "slug": "stickers-to-spell-word", + "leetcodeId": 691, + "title": "Stickers to Spell Word", + "difficulty": "Hard" }, { - "slug": "lowest-common-ancestor-of-a-binary-search-tree", - "leetcodeId": 235, - "title": "Lowest Common Ancestor of a BST", + "slug": "best-time-to-buy-and-sell-stock-with-transaction-fee", + "leetcodeId": 714, + "title": "Best Time to Buy and Sell Stock with Transaction Fee", "difficulty": "Medium" }, { - "slug": "binary-tree-level-order-traversal", - "leetcodeId": 102, - "title": "Binary Tree Level Order Traversal", + "slug": "delete-and-earn", + "leetcodeId": 740, + "title": "Delete and Earn", "difficulty": "Medium" }, { - "slug": "binary-tree-right-side-view", - "leetcodeId": 199, - "title": "Binary Tree Right Side View", - "difficulty": "Medium" + "slug": "min-cost-climbing-stairs", + "leetcodeId": 746, + "title": "Min Cost Climbing Stairs", + "difficulty": "Easy" }, { - "slug": "count-good-nodes-in-binary-tree", - "leetcodeId": 1448, - "title": "Count Good Nodes in Binary Tree", + "slug": "new-21-game", + "leetcodeId": 837, + "title": "New 21 Game", "difficulty": "Medium" }, { - "slug": "validate-binary-search-tree", - "leetcodeId": 98, - "title": "Validate Binary Search Tree", + "slug": "maximum-sum-circular-subarray", + "leetcodeId": 918, + "title": "Maximum Sum Circular Subarray", "difficulty": "Medium" }, { - "slug": "kth-smallest-element-in-a-bst", - "leetcodeId": 230, - "title": "Kth Smallest Element in a BST", + "slug": "minimum-cost-for-tickets", + "leetcodeId": 983, + "title": "Minimum Cost For Tickets", "difficulty": "Medium" }, { - "slug": "construct-binary-tree-from-preorder-and-inorder-traversal", - "leetcodeId": 105, - "title": "Construct Binary Tree from Preorder and Inorder Traversal", + "slug": "uncrossed-lines", + "leetcodeId": 1035, + "title": "Uncrossed Lines", "difficulty": "Medium" }, { - "slug": "binary-tree-maximum-path-sum", - "leetcodeId": 124, - "title": "Binary Tree Maximum Path Sum", - "difficulty": "Hard" + "slug": "n-th-tribonacci-number", + "leetcodeId": 1137, + "title": "N-th Tribonacci Number", + "difficulty": "Easy" }, { - "slug": "serialize-and-deserialize-binary-tree", - "leetcodeId": 297, - "title": "Serialize and Deserialize Binary Tree", + "slug": "count-all-valid-pickup-and-delivery-options", + "leetcodeId": 1359, + "title": "Count all Valid Pickup and Delivery Options", "difficulty": "Hard" }, { - "slug": "lowest-common-ancestor-of-a-binary-tree", - "leetcodeId": 236, - "title": "Lowest Common Ancestor of a Binary Tree", - "difficulty": "Medium" + "slug": "stone-game-iii", + "leetcodeId": 1406, + "title": "Stone Game III", + "difficulty": "Hard" }, { - "slug": "binary-tree-zigzag-level-order-traversal", - "leetcodeId": 103, - "title": "Binary Tree Zigzag Level Order Traversal", + "slug": "best-team-with-no-conflicts", + "leetcodeId": 1626, + "title": "Best Team with no Conflicts", "difficulty": "Medium" }, { - "slug": "path-sum", - "leetcodeId": 112, - "title": "Path Sum", - "difficulty": "Easy" + "slug": "maximize-score-after-n-operations", + "leetcodeId": 1799, + "title": "Maximize Score after N Operations", + "difficulty": "Hard" }, { - "slug": "path-sum-ii", - "leetcodeId": 113, - "title": "Path Sum II", + "slug": "maximum-subarray-min-product", + "leetcodeId": 1856, + "title": "Maximum Subarray Min Product", "difficulty": "Medium" }, { - "slug": "sum-root-to-leaf-numbers", - "leetcodeId": 129, - "title": "Sum Root to Leaf Numbers", - "difficulty": "Medium" + "slug": "find-the-longest-valid-obstacle-course-at-each-position", + "leetcodeId": 1964, + "title": "Find the Longest Valid Obstacle Course at Each Position", + "difficulty": "Hard" }, { - "slug": "path-sum-iii", - "leetcodeId": 437, - "title": "Path Sum III", + "slug": "solving-questions-with-brainpower", + "leetcodeId": 2140, + "title": "Solving Questions With Brainpower", "difficulty": "Medium" }, { - "slug": "binary-tree-inorder-traversal", - "leetcodeId": 94, - "title": "Binary Tree Inorder Traversal", - "difficulty": "Easy" - }, - { - "slug": "flatten-binary-tree-to-linked-list", - "leetcodeId": 114, - "title": "Flatten Binary Tree to Linked List", - "difficulty": "Medium" - } - ] - }, - { - "name": "Tries", - "problems": [ - { - "slug": "implement-trie-prefix-tree", - "leetcodeId": 208, - "title": "Implement Trie (Prefix Tree)", + "slug": "check-if-there-is-a-valid-partition-for-the-array", + "leetcodeId": 2369, + "title": "Check if There is a Valid Partition For The Array", "difficulty": "Medium" }, { - "slug": "design-add-and-search-words-data-structure", - "leetcodeId": 211, - "title": "Design Add and Search Words Data Structure", + "slug": "count-ways-to-build-good-strings", + "leetcodeId": 2466, + "title": "Count Ways to Build Good Strings", "difficulty": "Medium" - }, - { - "slug": "word-search-ii", - "leetcodeId": 212, - "title": "Word Search II", - "difficulty": "Hard" - }, - { - "slug": "prefix-and-suffix-search", - "leetcodeId": 745, - "title": "Prefix and Suffix Search", - "difficulty": "Hard" } ] }, { - "name": "Heaps / Priority Queues", + "name": "2-D Dynamic Programming", "problems": [ { - "slug": "kth-largest-element-in-a-stream", - "leetcodeId": 703, - "title": "Kth Largest Element in a Stream", - "difficulty": "Easy" + "slug": "regular-expression-matching", + "leetcodeId": 10, + "title": "Regular Expression Matching", + "difficulty": "Hard" }, { - "slug": "last-stone-weight", - "leetcodeId": 1046, - "title": "Last Stone Weight", - "difficulty": "Easy" + "slug": "wildcard-matching", + "leetcodeId": 44, + "title": "Wildcard Matching", + "difficulty": "Hard" }, { - "slug": "k-closest-points-to-origin", - "leetcodeId": 973, - "title": "K Closest Points to Origin", + "slug": "unique-paths", + "leetcodeId": 62, + "title": "Unique Paths", "difficulty": "Medium" }, { - "slug": "kth-largest-element-in-an-array", - "leetcodeId": 215, - "title": "Kth Largest Element in an Array", + "slug": "unique-paths-ii", + "leetcodeId": 63, + "title": "Unique Paths II", "difficulty": "Medium" }, { - "slug": "task-scheduler", - "leetcodeId": 621, - "title": "Task Scheduler", + "slug": "minimum-path-sum", + "leetcodeId": 64, + "title": "Minimum Path Sum", "difficulty": "Medium" }, { - "slug": "design-twitter", - "leetcodeId": 355, - "title": "Design Twitter", + "slug": "edit-distance", + "leetcodeId": 72, + "title": "Edit Distance", "difficulty": "Medium" }, { - "slug": "find-median-from-data-stream", - "leetcodeId": 295, - "title": "Find Median from Data Stream", + "slug": "interleaving-string", + "leetcodeId": 97, + "title": "Interleaving String", + "difficulty": "Medium" + }, + { + "slug": "distinct-subsequences", + "leetcodeId": 115, + "title": "Distinct Subsequences", "difficulty": "Hard" }, { - "slug": "reorganize-string", - "leetcodeId": 767, - "title": "Reorganize String", + "slug": "triangle", + "leetcodeId": 120, + "title": "Triangle", "difficulty": "Medium" }, { - "slug": "furthest-building-you-can-reach", - "leetcodeId": 1642, - "title": "Furthest Building You Can Reach", + "slug": "maximal-square", + "leetcodeId": 221, + "title": "Maximal Square", "difficulty": "Medium" }, { - "slug": "kth-smallest-element-in-a-sorted-matrix", - "leetcodeId": 378, - "title": "Kth Smallest Element in a Sorted Matrix", + "slug": "best-time-to-buy-and-sell-stock-with-cooldown", + "leetcodeId": 309, + "title": "Best Time to Buy and Sell Stock with Cooldown", "difficulty": "Medium" }, { - "slug": "smallest-number-in-infinite-set", - "leetcodeId": 2336, - "title": "Smallest Number in Infinite Set", - "difficulty": "Medium" + "slug": "burst-balloons", + "leetcodeId": 312, + "title": "Burst Balloons", + "difficulty": "Hard" }, { - "slug": "maximum-frequency-stack", - "leetcodeId": 895, - "title": "Maximum Frequency Stack", + "slug": "longest-increasing-path-in-a-matrix", + "leetcodeId": 329, + "title": "Longest Increasing Path in a Matrix", "difficulty": "Hard" - } - ] - }, - { - "name": "Backtracking", - "problems": [ + }, { - "slug": "subsets", - "leetcodeId": 78, - "title": "Subsets", + "slug": "ones-and-zeroes", + "leetcodeId": 474, + "title": "Ones and Zeroes", "difficulty": "Medium" }, { - "slug": "combination-sum", - "leetcodeId": 39, - "title": "Combination Sum", + "slug": "target-sum", + "leetcodeId": 494, + "title": "Target Sum", "difficulty": "Medium" }, { - "slug": "permutations", - "leetcodeId": 46, - "title": "Permutations", + "slug": "longest-palindromic-subsequence", + "leetcodeId": 516, + "title": "Longest Palindromic Subsequence", "difficulty": "Medium" }, { - "slug": "subsets-ii", - "leetcodeId": 90, - "title": "Subsets II", + "slug": "coin-change-ii", + "leetcodeId": 518, + "title": "Coin Change II", "difficulty": "Medium" }, { - "slug": "combination-sum-ii", - "leetcodeId": 40, - "title": "Combination Sum II", + "slug": "maximum-length-of-repeated-subarray", + "leetcodeId": 718, + "title": "Maximum Length of Repeated Subarray", "difficulty": "Medium" }, { - "slug": "word-search", - "leetcodeId": 79, - "title": "Word Search", + "slug": "stone-game", + "leetcodeId": 877, + "title": "Stone Game", "difficulty": "Medium" }, { - "slug": "palindrome-partitioning", - "leetcodeId": 131, - "title": "Palindrome Partitioning", - "difficulty": "Medium" + "slug": "profitable-schemes", + "leetcodeId": 879, + "title": "Profitable Schemes", + "difficulty": "Hard" }, { - "slug": "letter-combinations-of-a-phone-number", - "leetcodeId": 17, - "title": "Letter Combinations of a Phone Number", + "slug": "number-of-music-playlists", + "leetcodeId": 920, + "title": "Number of Music Playlists", + "difficulty": "Hard" + }, + { + "slug": "flip-string-to-monotone-increasing", + "leetcodeId": 926, + "title": "Flip String to Monotone Increasing", "difficulty": "Medium" }, { - "slug": "n-queens", - "leetcodeId": 51, - "title": "N-Queens", - "difficulty": "Hard" + "slug": "last-stone-weight-ii", + "leetcodeId": 1049, + "title": "Last Stone Weight II", + "difficulty": "Medium" }, { - "slug": "permutations-ii", - "leetcodeId": 47, - "title": "Permutations II", + "slug": "stone-game-ii", + "leetcodeId": 1140, + "title": "Stone Game II", "difficulty": "Medium" }, { - "slug": "combinations", - "leetcodeId": 77, - "title": "Combinations", + "slug": "longest-common-subsequence", + "leetcodeId": 1143, + "title": "Longest Common Subsequence", "difficulty": "Medium" }, { - "slug": "n-queens-ii", - "leetcodeId": 52, - "title": "N-Queens II", + "slug": "count-vowels-permutation", + "leetcodeId": 1220, + "title": "Count Vowels Permutation", "difficulty": "Hard" }, { - "slug": "sudoku-solver", - "leetcodeId": 37, - "title": "Sudoku Solver", + "slug": "minimum-cost-to-cut-a-stick", + "leetcodeId": 1547, + "title": "Minimum Cost to Cut a Stick", "difficulty": "Hard" }, { - "slug": "restore-ip-addresses", - "leetcodeId": 93, - "title": "Restore IP Addresses", - "difficulty": "Medium" + "slug": "number-of-ways-to-form-a-target-string-given-a-dictionary", + "leetcodeId": 1639, + "title": "Number of Ways to Form a Target String Given a Dictionary", + "difficulty": "Hard" }, { - "slug": "matchsticks-to-square", - "leetcodeId": 473, - "title": "Matchsticks to Square", - "difficulty": "Medium" + "slug": "number-of-ways-to-rearrange-sticks-with-k-sticks-visible", + "leetcodeId": 1866, + "title": "Number of Ways to Rearrange Sticks With K Sticks Visible", + "difficulty": "Hard" }, { - "slug": "partition-to-k-equal-sum-subsets", - "leetcodeId": 698, - "title": "Partition to K Equal Sum Subsets", - "difficulty": "Medium" + "slug": "maximum-value-of-k-coins-from-piles", + "leetcodeId": 2218, + "title": "Maximum Value of K Coins from Piles", + "difficulty": "Hard" }, { - "slug": "beautiful-arrangement", - "leetcodeId": 526, - "title": "Beautiful Arrangement", + "slug": "maximum-alternating-subsequence-sum", + "leetcodeId": 5782, + "title": "Maximum Alternating Subsequence Sum", "difficulty": "Medium" } ] }, { - "name": "Graphs", + "name": "Greedy", "problems": [ { - "slug": "number-of-islands", - "leetcodeId": 200, - "title": "Number of Islands", + "slug": "jump-game-ii", + "leetcodeId": 45, + "title": "Jump Game II", "difficulty": "Medium" }, { - "slug": "clone-graph", - "leetcodeId": 133, - "title": "Clone Graph", + "slug": "maximum-subarray", + "leetcodeId": 53, + "title": "Maximum Subarray", "difficulty": "Medium" }, { - "slug": "max-area-of-island", - "leetcodeId": 695, - "title": "Max Area of Island", + "slug": "jump-game", + "leetcodeId": 55, + "title": "Jump Game", "difficulty": "Medium" }, { - "slug": "pacific-atlantic-water-flow", - "leetcodeId": 417, - "title": "Pacific Atlantic Water Flow", + "slug": "gas-station", + "leetcodeId": 134, + "title": "Gas Station", "difficulty": "Medium" }, { - "slug": "surrounded-regions", - "leetcodeId": 130, - "title": "Surrounded Regions", - "difficulty": "Medium" + "slug": "candy", + "leetcodeId": 135, + "title": "Candy", + "difficulty": "Hard" }, { - "slug": "rotting-oranges", - "leetcodeId": 994, - "title": "Rotting Oranges", + "slug": "non-overlapping-intervals", + "leetcodeId": 435, + "title": "Non-overlapping Intervals", "difficulty": "Medium" }, { - "slug": "walls-and-gates", - "leetcodeId": 286, - "title": "Walls and Gates", + "slug": "minimum-number-of-arrows-to-burst-balloons", + "leetcodeId": 452, + "title": "Minimum Number of Arrows to Burst Balloons", "difficulty": "Medium" }, { - "slug": "course-schedule", - "leetcodeId": 207, - "title": "Course Schedule", - "difficulty": "Medium" + "slug": "assign-cookies", + "leetcodeId": 455, + "title": "Assign Cookies", + "difficulty": "Easy" }, { - "slug": "course-schedule-ii", - "leetcodeId": 210, - "title": "Course Schedule II", + "slug": "can-place-flowers", + "leetcodeId": 605, + "title": "Can Place Flowers", + "difficulty": "Easy" + }, + { + "slug": "maximum-length-of-pair-chain", + "leetcodeId": 646, + "title": "Maximum Length of Pair Chain", "difficulty": "Medium" }, { - "slug": "redundant-connection", - "leetcodeId": 684, - "title": "Redundant Connection", + "slug": "dota2-senate", + "leetcodeId": 649, + "title": "Dota2 Senate", "difficulty": "Medium" }, { - "slug": "number-of-connected-components-in-an-undirected-graph", - "leetcodeId": 323, - "title": "Number of Connected Components in an Undirected Graph", + "slug": "valid-parenthesis-string", + "leetcodeId": 678, + "title": "Valid Parenthesis String", "difficulty": "Medium" }, { - "slug": "graph-valid-tree", - "leetcodeId": 261, - "title": "Graph Valid Tree", + "slug": "partition-labels", + "leetcodeId": 763, + "title": "Partition Labels", "difficulty": "Medium" }, { - "slug": "word-ladder", - "leetcodeId": 127, - "title": "Word Ladder", - "difficulty": "Hard" + "slug": "hand-of-straights", + "leetcodeId": 846, + "title": "Hand of Straights", + "difficulty": "Medium" }, { - "slug": "shortest-path-in-binary-matrix", - "leetcodeId": 1091, - "title": "Shortest Path in Binary Matrix", + "slug": "longest-turbulent-subarray", + "leetcodeId": 978, + "title": "Longest Turbulent Array", "difficulty": "Medium" }, { - "slug": "01-matrix", - "leetcodeId": 542, - "title": "01 Matrix", + "slug": "two-city-scheduling", + "leetcodeId": 1029, + "title": "Two City Scheduling", "difficulty": "Medium" }, { - "slug": "shortest-bridge", - "leetcodeId": 934, - "title": "Shortest Bridge", + "slug": "minimum-cost-to-connect-sticks", + "leetcodeId": 1167, + "title": "Minimum Cost to Connect Sticks", "difficulty": "Medium" }, { - "slug": "accounts-merge", - "leetcodeId": 721, - "title": "Accounts Merge", + "slug": "maximum-points-you-can-obtain-from-cards", + "leetcodeId": 1423, + "title": "Maximum Points You Can Obtain From Cards", + "difficulty": "Medium" + }, + { + "slug": "minimum-deletions-to-make-character-frequencies-unique", + "leetcodeId": 1647, + "title": "Minimum Deletions to Make Character Frequencies Unique", "difficulty": "Medium" }, { - "slug": "find-the-town-judge", - "leetcodeId": 997, - "title": "Find the Town Judge", + "slug": "maximum-units-on-a-truck", + "leetcodeId": 1710, + "title": "Maximum Units on a Truck", "difficulty": "Easy" }, { - "slug": "evaluate-division", - "leetcodeId": 399, - "title": "Evaluate Division", + "slug": "jump-game-vii", + "leetcodeId": 1871, + "title": "Jump Game VII", "difficulty": "Medium" }, { - "slug": "keys-and-rooms", - "leetcodeId": 841, - "title": "Keys and Rooms", + "slug": "merge-triplets-to-form-target-triplet", + "leetcodeId": 1899, + "title": "Merge Triplets to Form Target Triplet", "difficulty": "Medium" }, { - "slug": "island-perimeter", - "leetcodeId": 463, - "title": "Island Perimeter", - "difficulty": "Easy" + "slug": "eliminate-maximum-number-of-monsters", + "leetcodeId": 1921, + "title": "Eliminate Maximum Number of Monsters", + "difficulty": "Medium" + }, + { + "slug": "minimize-maximum-of-array", + "leetcodeId": 2439, + "title": "Minimize Maximum of Array", + "difficulty": "Medium" } ] }, { - "name": "Advanced Graphs", + "name": "Intervals", "problems": [ { - "slug": "reconstruct-itinerary", - "leetcodeId": 332, - "title": "Reconstruct Itinerary", - "difficulty": "Hard" - }, - { - "slug": "min-cost-to-connect-all-points", - "leetcodeId": 1584, - "title": "Min Cost to Connect All Points", + "slug": "merge-intervals", + "leetcodeId": 56, + "title": "Merge Intervals", "difficulty": "Medium" }, { - "slug": "network-delay-time", - "leetcodeId": 743, - "title": "Network Delay Time", + "slug": "insert-interval", + "leetcodeId": 57, + "title": "Insert Interval", "difficulty": "Medium" }, { - "slug": "swim-in-rising-water", - "leetcodeId": 778, - "title": "Swim in Rising Water", - "difficulty": "Hard" - }, - { - "slug": "alien-dictionary", - "leetcodeId": 269, - "title": "Alien Dictionary", - "difficulty": "Hard" + "slug": "summary-ranges", + "leetcodeId": 228, + "title": "Summary Ranges", + "difficulty": "Easy" }, { - "slug": "cheapest-flights-within-k-stops", - "leetcodeId": 787, - "title": "Cheapest Flights Within K Stops", - "difficulty": "Medium" + "slug": "meeting-rooms", + "leetcodeId": 252, + "title": "Meeting Rooms", + "difficulty": "Easy" }, { - "slug": "find-the-city-with-the-smallest-number-of-neighbors-at-a-threshold-distance", - "leetcodeId": 1334, - "title": "Find the City With the Smallest Number of Neighbors at a Threshold Distance", + "slug": "meeting-rooms-ii", + "leetcodeId": 253, + "title": "Meeting Rooms II", "difficulty": "Medium" }, { - "slug": "critical-connections-in-a-network", - "leetcodeId": 1192, - "title": "Critical Connections in a Network", + "slug": "data-stream-as-disjoint-intervals", + "leetcodeId": 352, + "title": "Data Stream as Disjoint Intervals", "difficulty": "Hard" }, { - "slug": "parallel-courses-iii", - "leetcodeId": 2050, - "title": "Parallel Courses III", - "difficulty": "Hard" + "slug": "non-overlapping-intervals", + "leetcodeId": 435, + "title": "Non-overlapping Intervals", + "difficulty": "Medium" }, { - "slug": "path-with-minimum-effort", - "leetcodeId": 1631, - "title": "Path With Minimum Effort", + "slug": "remove-covered-intervals", + "leetcodeId": 1288, + "title": "Remove Covered Intervals", "difficulty": "Medium" + }, + { + "slug": "minimum-interval-to-include-each-query", + "leetcodeId": 1851, + "title": "Minimum Interval to Include Each Query", + "difficulty": "Hard" } ] }, { - "name": "1-D Dynamic Programming", + "name": "Math & Geometry", "problems": [ { - "slug": "climbing-stairs", - "leetcodeId": 70, - "title": "Climbing Stairs", - "difficulty": "Easy" + "slug": "zigzag-conversion", + "leetcodeId": 6, + "title": "Zigzag Conversion", + "difficulty": "Medium" }, { - "slug": "min-cost-climbing-stairs", - "leetcodeId": 746, - "title": "Min Cost Climbing Stairs", + "slug": "palindrome-number", + "leetcodeId": 9, + "title": "Palindrome Number", "difficulty": "Easy" }, { - "slug": "house-robber", - "leetcodeId": 198, - "title": "House Robber", + "slug": "integer-to-roman", + "leetcodeId": 12, + "title": "Integer to Roman", "difficulty": "Medium" }, { - "slug": "house-robber-ii", - "leetcodeId": 213, - "title": "House Robber II", - "difficulty": "Medium" + "slug": "roman-to-integer", + "leetcodeId": 13, + "title": "Roman to Integer", + "difficulty": "Easy" }, { - "slug": "longest-palindromic-substring", - "leetcodeId": 5, - "title": "Longest Palindromic Substring", + "slug": "multiply-strings", + "leetcodeId": 43, + "title": "Multiply Strings", "difficulty": "Medium" }, { - "slug": "palindromic-substrings", - "leetcodeId": 647, - "title": "Palindromic Substrings", + "slug": "rotate-image", + "leetcodeId": 48, + "title": "Rotate Image", "difficulty": "Medium" }, { - "slug": "decode-ways", - "leetcodeId": 91, - "title": "Decode Ways", + "slug": "pow-x-n", + "leetcodeId": 50, + "title": "Pow(x, n)", "difficulty": "Medium" }, { - "slug": "coin-change", - "leetcodeId": 322, - "title": "Coin Change", + "slug": "powx-n", + "leetcodeId": 50, + "title": "Pow(x, n)", "difficulty": "Medium" }, { - "slug": "maximum-product-subarray", - "leetcodeId": 152, - "title": "Maximum Product Subarray", + "slug": "spiral-matrix", + "leetcodeId": 54, + "title": "Spiral Matrix", "difficulty": "Medium" }, { - "slug": "word-break", - "leetcodeId": 139, - "title": "Word Break", + "slug": "spiral-matrix-ii", + "leetcodeId": 59, + "title": "Spiral Matrix II", "difficulty": "Medium" }, { - "slug": "longest-increasing-subsequence", - "leetcodeId": 300, - "title": "Longest Increasing Subsequence", - "difficulty": "Medium" + "slug": "plus-one", + "leetcodeId": 66, + "title": "Plus One", + "difficulty": "Easy" }, { - "slug": "partition-equal-subset-sum", - "leetcodeId": 416, - "title": "Partition Equal Subset Sum", + "slug": "add-binary", + "leetcodeId": 67, + "title": "Add Binary", + "difficulty": "Easy" + }, + { + "slug": "set-matrix-zeroes", + "leetcodeId": 73, + "title": "Set Matrix Zeroes", "difficulty": "Medium" }, { - "slug": "fibonacci-number", - "leetcodeId": 509, - "title": "Fibonacci Number", + "slug": "max-points-on-a-line", + "leetcodeId": 149, + "title": "Maximum Points on a Line", + "difficulty": "Hard" + }, + { + "slug": "excel-sheet-column-title", + "leetcodeId": 168, + "title": "Excel Sheet Column Title", "difficulty": "Easy" }, { - "slug": "n-th-tribonacci-number", - "leetcodeId": 1137, - "title": "N-th Tribonacci Number", + "slug": "happy-number", + "leetcodeId": 202, + "title": "Happy Number", "difficulty": "Easy" }, { - "slug": "delete-and-earn", - "leetcodeId": 740, - "title": "Delete and Earn", + "slug": "rectangle-area", + "leetcodeId": 223, + "title": "Rectangle Area", "difficulty": "Medium" }, { - "slug": "maximum-sum-circular-subarray", - "leetcodeId": 918, - "title": "Maximum Sum Circular Subarray", - "difficulty": "Medium" + "slug": "ugly-number", + "leetcodeId": 263, + "title": "Ugly Number", + "difficulty": "Easy" }, { - "slug": "best-time-to-buy-and-sell-stock-with-transaction-fee", - "leetcodeId": 714, - "title": "Best Time to Buy and Sell Stock with Transaction Fee", + "slug": "valid-square", + "leetcodeId": 593, + "title": "Valid Square", "difficulty": "Medium" - } - ] - }, - { - "name": "2-D Dynamic Programming", - "problems": [ + }, { - "slug": "unique-paths", - "leetcodeId": 62, - "title": "Unique Paths", + "slug": "robot-bounded-in-circle", + "leetcodeId": 1041, + "title": "Robot Bounded In Circle", "difficulty": "Medium" }, { - "slug": "longest-common-subsequence", - "leetcodeId": 1143, - "title": "Longest Common Subsequence", - "difficulty": "Medium" + "slug": "greatest-common-divisor-of-strings", + "leetcodeId": 1071, + "title": "Greatest Common Divisor of Strings", + "difficulty": "Easy" }, { - "slug": "best-time-to-buy-and-sell-stock-with-cooldown", - "leetcodeId": 309, - "title": "Best Time to Buy and Sell Stock with Cooldown", + "slug": "number-of-dice-rolls-with-target-sum", + "leetcodeId": 1155, + "title": "Number of Dice Rolls With Target Sum", "difficulty": "Medium" }, { - "slug": "coin-change-ii", - "leetcodeId": 518, - "title": "Coin Change II", - "difficulty": "Medium" + "slug": "shift-2d-grid", + "leetcodeId": 1260, + "title": "Shift 2D Grid", + "difficulty": "Easy" }, { - "slug": "target-sum", - "leetcodeId": 494, - "title": "Target Sum", - "difficulty": "Medium" + "slug": "count-odd-numbers-in-an-interval-range", + "leetcodeId": 1523, + "title": "Count Odd Numbers in an Interval Range", + "difficulty": "Easy" }, { - "slug": "interleaving-string", - "leetcodeId": 97, - "title": "Interleaving String", + "slug": "matrix-diagonal-sum", + "leetcodeId": 1572, + "title": "Matrix Diagonal Sum", + "difficulty": "Easy" + }, + { + "slug": "detect-squares", + "leetcodeId": 2013, + "title": "Detect Squares", "difficulty": "Medium" }, { - "slug": "longest-increasing-path-in-a-matrix", - "leetcodeId": 329, - "title": "Longest Increasing Path in a Matrix", - "difficulty": "Hard" + "slug": "find-missing-observations", + "leetcodeId": 2028, + "title": "Find Missing Observations", + "difficulty": "Medium" + } + ] + }, + { + "name": "Bit Manipulation", + "problems": [ + { + "slug": "reverse-integer", + "leetcodeId": 7, + "title": "Reverse Integer", + "difficulty": "Medium" }, { - "slug": "distinct-subsequences", - "leetcodeId": 115, - "title": "Distinct Subsequences", - "difficulty": "Hard" + "slug": "single-number", + "leetcodeId": 136, + "title": "Single Number", + "difficulty": "Easy" }, { - "slug": "edit-distance", - "leetcodeId": 72, - "title": "Edit Distance", + "slug": "single-number-ii", + "leetcodeId": 137, + "title": "Single Number II", "difficulty": "Medium" }, { - "slug": "burst-balloons", - "leetcodeId": 312, - "title": "Burst Balloons", - "difficulty": "Hard" + "slug": "reverse-bits", + "leetcodeId": 190, + "title": "Reverse Bits", + "difficulty": "Easy" }, { - "slug": "regular-expression-matching", - "leetcodeId": 10, - "title": "Regular Expression Matching", - "difficulty": "Hard" + "slug": "number-of-1-bits", + "leetcodeId": 191, + "title": "Number of 1 Bits", + "difficulty": "Easy" }, { - "slug": "unique-paths-ii", - "leetcodeId": 63, - "title": "Unique Paths II", + "slug": "bitwise-and-of-numbers-range", + "leetcodeId": 201, + "title": "Bitwise AND of Numbers Range", "difficulty": "Medium" }, { - "slug": "minimum-path-sum", - "leetcodeId": 64, - "title": "Minimum Path Sum", + "slug": "single-number-iii", + "leetcodeId": 260, + "title": "Single Number III", "difficulty": "Medium" }, { - "slug": "triangle", - "leetcodeId": 120, - "title": "Triangle", - "difficulty": "Medium" + "slug": "missing-number", + "leetcodeId": 268, + "title": "Missing Number", + "difficulty": "Easy" }, { - "slug": "maximum-length-of-repeated-subarray", - "leetcodeId": 718, - "title": "Maximum Length of Repeated Subarray", + "slug": "counting-bits", + "leetcodeId": 338, + "title": "Counting Bits", + "difficulty": "Easy" + }, + { + "slug": "sum-of-two-integers", + "leetcodeId": 371, + "title": "Sum of Two Integers", "difficulty": "Medium" }, { - "slug": "wildcard-matching", - "leetcodeId": 44, - "title": "Wildcard Matching", - "difficulty": "Hard" + "slug": "add-to-array-form-of-integer", + "leetcodeId": 989, + "title": "Add to Array-Form of Integer", + "difficulty": "Easy" + }, + { + "slug": "shuffle-the-array", + "leetcodeId": 1470, + "title": "Shuffle the Array", + "difficulty": "Easy" } ] }, { - "name": "Greedy", + "name": "JavaScript", "problems": [ { - "slug": "maximum-subarray", - "leetcodeId": 53, - "title": "Maximum Subarray", + "slug": "check-if-object-instance-of-class", + "leetcodeId": 2618, + "title": "Check if Object Instance of Class", "difficulty": "Medium" }, { - "slug": "jump-game", - "leetcodeId": 55, - "title": "Jump Game", - "difficulty": "Medium" + "slug": "array-prototype-last", + "leetcodeId": 2619, + "title": "Array Prototype Last", + "difficulty": "Easy" }, { - "slug": "jump-game-ii", - "leetcodeId": 45, - "title": "Jump Game II", - "difficulty": "Medium" + "slug": "counter", + "leetcodeId": 2620, + "title": "Counter", + "difficulty": "Easy" }, { - "slug": "gas-station", - "leetcodeId": 134, - "title": "Gas Station", - "difficulty": "Medium" + "slug": "create-counter", + "leetcodeId": 2620, + "title": "Counter", + "difficulty": "Easy" }, { - "slug": "hand-of-straights", - "leetcodeId": 846, - "title": "Hand of Straights", - "difficulty": "Medium" + "slug": "sleep", + "leetcodeId": 2621, + "title": "Sleep", + "difficulty": "Easy" }, { - "slug": "merge-triplets-to-form-target-triplet", - "leetcodeId": 1899, - "title": "Merge Triplets to Form Target Triplet", + "slug": "cache-with-time-limit", + "leetcodeId": 2622, + "title": "Cache With Time Limit", "difficulty": "Medium" }, { - "slug": "partition-labels", - "leetcodeId": 763, - "title": "Partition Labels", + "slug": "memoize", + "leetcodeId": 2623, + "title": "Memoize", "difficulty": "Medium" }, { - "slug": "valid-parenthesis-string", - "leetcodeId": 678, - "title": "Valid Parenthesis String", + "slug": "flatten-deeply-nested-array", + "leetcodeId": 2625, + "title": "Flatten Deeply Nested Array", "difficulty": "Medium" }, { - "slug": "maximum-units-on-a-truck", - "leetcodeId": 1710, - "title": "Maximum Units on a Truck", + "slug": "array-reduce-transformation", + "leetcodeId": 2626, + "title": "Array Reduce Transformation", "difficulty": "Easy" }, { - "slug": "minimum-number-of-arrows-to-burst-balloons", - "leetcodeId": 452, - "title": "Minimum Number of Arrows to Burst Balloons", + "slug": "debounce", + "leetcodeId": 2627, + "title": "Debounce", "difficulty": "Medium" }, { - "slug": "non-overlapping-intervals", - "leetcodeId": 435, - "title": "Non-overlapping Intervals", + "slug": "json-deep-equal", + "leetcodeId": 2628, + "title": "JSON Deep Equal", "difficulty": "Medium" }, { - "slug": "can-place-flowers", - "leetcodeId": 605, - "title": "Can Place Flowers", + "slug": "function-composition", + "leetcodeId": 2629, + "title": "Function Composition", "difficulty": "Easy" }, { - "slug": "minimum-cost-to-connect-sticks", - "leetcodeId": 1167, - "title": "Minimum Cost to Connect Sticks", + "slug": "group-by", + "leetcodeId": 2631, + "title": "Group By", "difficulty": "Medium" }, { - "slug": "assign-cookies", - "leetcodeId": 455, - "title": "Assign Cookies", - "difficulty": "Easy" - } - ] - }, - { - "name": "Intervals", - "problems": [ - { - "slug": "insert-interval", - "leetcodeId": 57, - "title": "Insert Interval", + "slug": "curry", + "leetcodeId": 2632, + "title": "Curry", "difficulty": "Medium" }, { - "slug": "merge-intervals", - "leetcodeId": 56, - "title": "Merge Intervals", + "slug": "convert-object-to-json-string", + "leetcodeId": 2633, + "title": "Convert Object to JSON String", "difficulty": "Medium" }, { - "slug": "non-overlapping-intervals", - "leetcodeId": 435, - "title": "Non-overlapping Intervals", - "difficulty": "Medium" + "slug": "filter-elements-from-array", + "leetcodeId": 2634, + "title": "Filter Elements from Array", + "difficulty": "Easy" }, { - "slug": "meeting-rooms", - "leetcodeId": 252, - "title": "Meeting Rooms", + "slug": "apply-transform-over-each-element-in-array", + "leetcodeId": 2635, + "title": "Apply Transform Over Each Element in Array", "difficulty": "Easy" }, { - "slug": "meeting-rooms-ii", - "leetcodeId": 253, - "title": "Meeting Rooms II", + "slug": "promise-pool", + "leetcodeId": 2636, + "title": "Promise Pool", "difficulty": "Medium" }, { - "slug": "minimum-interval-to-include-each-query", - "leetcodeId": 1851, - "title": "Minimum Interval to Include Each Query", - "difficulty": "Hard" + "slug": "promise-time-limit", + "leetcodeId": 2637, + "title": "Promise Time Limit", + "difficulty": "Medium" }, { - "slug": "summary-ranges", - "leetcodeId": 228, - "title": "Summary Ranges", + "slug": "generate-fibonacci-sequence", + "leetcodeId": 2648, + "title": "Generate Fibonacci Sequence", "difficulty": "Easy" - } - ] - }, - { - "name": "Math & Geometry", - "problems": [ - { - "slug": "rotate-image", - "leetcodeId": 48, - "title": "Rotate Image", - "difficulty": "Medium" }, { - "slug": "spiral-matrix", - "leetcodeId": 54, - "title": "Spiral Matrix", + "slug": "nested-array-generator", + "leetcodeId": 2649, + "title": "Nested Array Generator", "difficulty": "Medium" }, { - "slug": "set-matrix-zeroes", - "leetcodeId": 73, - "title": "Set Matrix Zeroes", - "difficulty": "Medium" + "slug": "counter-ii", + "leetcodeId": 2665, + "title": "Counter II", + "difficulty": "Easy" }, { - "slug": "happy-number", - "leetcodeId": 202, - "title": "Happy Number", + "slug": "allow-one-function-call", + "leetcodeId": 2666, + "title": "Allow One Function Call", "difficulty": "Easy" }, { - "slug": "plus-one", - "leetcodeId": 66, - "title": "Plus One", + "slug": "create-hello-world-function", + "leetcodeId": 2667, + "title": "Create Hello World Function", "difficulty": "Easy" }, { - "slug": "pow-x-n", - "leetcodeId": 50, - "title": "Pow(x, n)", + "slug": "array-of-objects-to-matrix", + "leetcodeId": 2675, + "title": "Array of Objects to Matrix", "difficulty": "Medium" }, { - "slug": "multiply-strings", - "leetcodeId": 43, - "title": "Multiply Strings", + "slug": "throttle", + "leetcodeId": 2676, + "title": "Throttle", "difficulty": "Medium" }, { - "slug": "detect-squares", - "leetcodeId": 2013, - "title": "Detect Squares", - "difficulty": "Medium" + "slug": "chunk-array", + "leetcodeId": 2677, + "title": "Chunk Array", + "difficulty": "Easy" }, { - "slug": "spiral-matrix-ii", - "leetcodeId": 59, - "title": "Spiral Matrix II", + "slug": "call-function-with-custom-context", + "leetcodeId": 2693, + "title": "Call Function with Custom Context", "difficulty": "Medium" }, { - "slug": "add-binary", - "leetcodeId": 67, - "title": "Add Binary", - "difficulty": "Easy" - }, - { - "slug": "rectangle-area", - "leetcodeId": 223, - "title": "Rectangle Area", + "slug": "event-emitter", + "leetcodeId": 2694, + "title": "Event Emitter", "difficulty": "Medium" }, { - "slug": "valid-square", - "leetcodeId": 593, - "title": "Valid Square", - "difficulty": "Medium" + "slug": "array-wrapper", + "leetcodeId": 2695, + "title": "Array Wrapper", + "difficulty": "Easy" }, { - "slug": "number-of-dice-rolls-with-target-sum", - "leetcodeId": 1155, - "title": "Number of Dice Rolls With Target Sum", + "slug": "differences-between-two-objects", + "leetcodeId": 2700, + "title": "Difference Between Two Objects", "difficulty": "Medium" - } - ] - }, - { - "name": "Bit Manipulation", - "problems": [ - { - "slug": "single-number", - "leetcodeId": 136, - "title": "Single Number", - "difficulty": "Easy" }, { - "slug": "number-of-1-bits", - "leetcodeId": 191, - "title": "Number of 1 Bits", + "slug": "return-length-of-arguments-passed", + "leetcodeId": 2703, + "title": "Return Length of Arguments Passed", "difficulty": "Easy" }, { - "slug": "counting-bits", - "leetcodeId": 338, - "title": "Counting Bits", + "slug": "to-be-or-not-to-be", + "leetcodeId": 2704, + "title": "To Be Or Not To Be", "difficulty": "Easy" }, { - "slug": "reverse-bits", - "leetcodeId": 190, - "title": "Reverse Bits", - "difficulty": "Easy" + "slug": "expect", + "leetcodeId": 2705, + "title": "Compact Object", + "difficulty": "Medium" }, { - "slug": "missing-number", - "leetcodeId": 268, - "title": "Missing Number", + "slug": "timeout-cancellation", + "leetcodeId": 2715, + "title": "Timeout Cancellation", "difficulty": "Easy" }, { - "slug": "sum-of-two-integers", - "leetcodeId": 371, - "title": "Sum of Two Integers", + "slug": "join-two-arrays-by-id", + "leetcodeId": 2722, + "title": "Join Two Arrays by ID", "difficulty": "Medium" }, { - "slug": "reverse-integer", - "leetcodeId": 7, - "title": "Reverse Integer", - "difficulty": "Medium" + "slug": "add-two-promises", + "leetcodeId": 2723, + "title": "Add Two Promises", + "difficulty": "Easy" }, { - "slug": "single-number-ii", - "leetcodeId": 137, - "title": "Single Number II", - "difficulty": "Medium" + "slug": "sort-by", + "leetcodeId": 2724, + "title": "Sort By", + "difficulty": "Easy" }, { - "slug": "single-number-iii", - "leetcodeId": 260, - "title": "Single Number III", + "slug": "interval-cancellation", + "leetcodeId": 2725, + "title": "Interval Cancellation", "difficulty": "Medium" }, { - "slug": "bitwise-and-of-numbers-range", - "leetcodeId": 201, - "title": "Bitwise AND of Numbers Range", - "difficulty": "Medium" + "slug": "calculator-with-method-chaining", + "leetcodeId": 2726, + "title": "Calculator with Method Chaining", + "difficulty": "Easy" } ] - }, - { - "name": "JavaScript", - "problems": [ - { "slug": "create-hello-world-function", "leetcodeId": 2667, "title": "Create Hello World Function", "difficulty": "Easy" }, - { "slug": "counter", "leetcodeId": 2620, "title": "Counter", "difficulty": "Easy" }, - { "slug": "counter-ii", "leetcodeId": 2665, "title": "Counter II", "difficulty": "Easy" }, - { "slug": "to-be-or-not-to-be", "leetcodeId": 2704, "title": "To Be Or Not To Be", "difficulty": "Easy" }, - { "slug": "expect", "leetcodeId": 2705, "title": "Compact Object", "difficulty": "Medium" }, - { "slug": "array-reduce-transformation", "leetcodeId": 2626, "title": "Array Reduce Transformation", "difficulty": "Easy" }, - { "slug": "function-composition", "leetcodeId": 2629, "title": "Function Composition", "difficulty": "Easy" }, - { "slug": "return-length-of-arguments-passed", "leetcodeId": 2703, "title": "Return Length of Arguments Passed", "difficulty": "Easy" }, - { "slug": "allow-one-function-call", "leetcodeId": 2666, "title": "Allow One Function Call", "difficulty": "Easy" }, - { "slug": "create-counter", "leetcodeId": 2620, "title": "Counter", "difficulty": "Easy" }, - { "slug": "sleep", "leetcodeId": 2621, "title": "Sleep", "difficulty": "Easy" }, - { "slug": "array-prototype-last", "leetcodeId": 2619, "title": "Array Prototype Last", "difficulty": "Easy" }, - { "slug": "apply-transform-over-each-element-in-array", "leetcodeId": 2635, "title": "Apply Transform Over Each Element in Array", "difficulty": "Easy" }, - { "slug": "filter-elements-from-array", "leetcodeId": 2634, "title": "Filter Elements from Array", "difficulty": "Easy" }, - { "slug": "array-wrapper", "leetcodeId": 2695, "title": "Array Wrapper", "difficulty": "Easy" }, - { "slug": "calculator-with-method-chaining", "leetcodeId": 2726, "title": "Calculator with Method Chaining", "difficulty": "Easy" }, - { "slug": "debounce", "leetcodeId": 2627, "title": "Debounce", "difficulty": "Medium" }, - { "slug": "throttle", "leetcodeId": 2676, "title": "Throttle", "difficulty": "Medium" }, - { "slug": "json-deep-equal", "leetcodeId": 2628, "title": "JSON Deep Equal", "difficulty": "Medium" }, - { "slug": "memoize", "leetcodeId": 2623, "title": "Memoize", "difficulty": "Medium" }, - { "slug": "add-two-promises", "leetcodeId": 2723, "title": "Add Two Promises", "difficulty": "Easy" }, - { "slug": "timeout-cancellation", "leetcodeId": 2715, "title": "Timeout Cancellation", "difficulty": "Easy" }, - { "slug": "interval-cancellation", "leetcodeId": 2725, "title": "Interval Cancellation", "difficulty": "Medium" }, - { "slug": "promise-time-limit", "leetcodeId": 2637, "title": "Promise Time Limit", "difficulty": "Medium" }, - { "slug": "cache-with-time-limit", "leetcodeId": 2622, "title": "Cache With Time Limit", "difficulty": "Medium" }, - { "slug": "flatten-deeply-nested-array", "leetcodeId": 2625, "title": "Flatten Deeply Nested Array", "difficulty": "Medium" }, - { "slug": "group-by", "leetcodeId": 2631, "title": "Group By", "difficulty": "Medium" }, - { "slug": "sort-by", "leetcodeId": 2724, "title": "Sort By", "difficulty": "Easy" }, - { "slug": "join-two-arrays-by-id", "leetcodeId": 2722, "title": "Join Two Arrays by ID", "difficulty": "Medium" }, - { "slug": "chunk-array", "leetcodeId": 2677, "title": "Chunk Array", "difficulty": "Easy" } - ] } ] } \ No newline at end of file diff --git a/src/background/index.js b/src/background/index.js index 8416906..7efe5d2 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -31,6 +31,14 @@ chrome.runtime.onStartup.addListener(async () => { // Set up message listener setupMessageListener(); +// Listen for exclusion list changes and update redirect rule +chrome.storage.onChanged.addListener(async (changes, areaName) => { + if (areaName === 'sync' && changes.userExclusionList) { + console.log("User exclusion list changed, updating redirect rule"); + await checkAndRestoreRedirect(); + } +}); + // Periodic checks (every minute) // Check for daily reset and restore redirects if bypass/daily solve expired setInterval(async () => { diff --git a/src/background/messageHandler.js b/src/background/messageHandler.js index 33a566c..4fb0006 100644 --- a/src/background/messageHandler.js +++ b/src/background/messageHandler.js @@ -103,6 +103,14 @@ async function handleProblemSolved(message) { // Mark daily solve with problem slug await storage.markDailySolve(message.slug); + // Add problem to solved problems list + const state = await storage.getState(); + const solvedProblems = new Set(state.solvedProblems || []); + solvedProblems.add(message.slug); + await chrome.storage.sync.set({ + solvedProblems: Array.from(solvedProblems) + }); + // Remove redirect rule to unblock websites await redirects.removeRedirectRule(); diff --git a/src/background/problemLogic.js b/src/background/problemLogic.js index b4c2b70..f8df036 100644 --- a/src/background/problemLogic.js +++ b/src/background/problemLogic.js @@ -252,59 +252,117 @@ export async function computeNextProblem(syncAllSolved = false) { } // Check if sorting by difficulty is enabled - const settings = await chrome.storage.sync.get(['sortByDifficulty']); + const settings = await chrome.storage.sync.get(['sortByDifficulty', 'randomProblemSelection']); const sortByDifficulty = settings.sortByDifficulty === true; + const randomProblemSelection = settings.randomProblemSelection === true; - // Second pass: Find first unsolved problem in order, starting from saved position - for (let catIdx = startCategoryIndex; catIdx < problemSet.categories.length; catIdx++) { - const category = problemSet.categories[catIdx]; + // If random selection is enabled, collect all unsolved problems and pick one randomly + if (randomProblemSelection) { + const unsolvedProblems = []; - // Get problems, sorted if needed - let problemsToCheck = category.problems; - if (sortByDifficulty) { - problemsToCheck = sortProblemsByDifficulty(category.problems); + // Collect all unsolved problems with their category and original index + for (let catIdx = 0; catIdx < problemSet.categories.length; catIdx++) { + const category = problemSet.categories[catIdx]; + + for (let probIdx = 0; probIdx < category.problems.length; probIdx++) { + const problem = category.problems[probIdx]; + + if (!solvedProblems.has(problem.slug)) { + unsolvedProblems.push({ + problem: problem, + categoryIndex: catIdx, + problemIndex: probIdx, + category: category + }); + } + } } - // Start from saved problem index if we're on the starting category - const startIdx = (catIdx === startCategoryIndex) ? startProblemIndex : 0; - - for (let probIdx = startIdx; probIdx < problemsToCheck.length; probIdx++) { - const problem = problemsToCheck[probIdx]; + // If there are unsolved problems, pick one randomly + if (unsolvedProblems.length > 0) { + const randomIndex = Math.floor(Math.random() * unsolvedProblems.length); + const selected = unsolvedProblems[randomIndex]; - if (!solvedProblems.has(problem.slug)) { - // Found first unsolved problem - // Find original index if sorted - let originalProbIdx = probIdx; - if (sortByDifficulty) { - originalProbIdx = category.problems.findIndex(p => p.slug === problem.slug); - } - - currentCategoryIndex = catIdx; - currentProblemIndex = originalProbIdx; - currentProblemSlug = problem.slug; - - // Save position for the selected set only - await saveState(catIdx, originalProbIdx, solvedProblems, selectedProblemSet); - - const totalProblems = problemSet.categories.reduce( - (sum, cat) => sum + cat.problems.length, - 0 - ); - - // Count only solved problems that are in the current problem set - const solvedCount = problemSet.categories.reduce((count, cat) => { - return count + cat.problems.filter(p => solvedProblems.has(p.slug)).length; - }, 0); + currentCategoryIndex = selected.categoryIndex; + currentProblemIndex = selected.problemIndex; + currentProblemSlug = selected.problem.slug; + + // Save position for the selected set (still track position for compatibility) + await saveState(selected.categoryIndex, selected.problemIndex, solvedProblems, selectedProblemSet); + + const totalProblems = problemSet.categories.reduce( + (sum, cat) => sum + cat.problems.length, + 0 + ); + + // Count only solved problems that are in the current problem set + const solvedCount = problemSet.categories.reduce((count, cat) => { + return count + cat.problems.filter(p => solvedProblems.has(p.slug)).length; + }, 0); + + return { + categoryIndex: selected.categoryIndex, + categoryName: selected.category.name, + problemIndex: selected.problemIndex, + problem: selected.problem, + totalProblems: totalProblems, + solvedCount: solvedCount, + categoryProgress: computeCategoryProgress(selected.category, solvedProblems), + }; + } + // If no unsolved problems, fall through to "all solved" logic below + } else { + // Sequential selection: Find first unsolved problem in order, starting from saved position + for (let catIdx = startCategoryIndex; catIdx < problemSet.categories.length; catIdx++) { + const category = problemSet.categories[catIdx]; + + // Get problems, sorted if needed + let problemsToCheck = category.problems; + if (sortByDifficulty) { + problemsToCheck = sortProblemsByDifficulty(category.problems); + } + + // Start from saved problem index if we're on the starting category + const startIdx = (catIdx === startCategoryIndex) ? startProblemIndex : 0; + + for (let probIdx = startIdx; probIdx < problemsToCheck.length; probIdx++) { + const problem = problemsToCheck[probIdx]; - return { - categoryIndex: catIdx, - categoryName: category.name, - problemIndex: originalProbIdx, - problem: problem, - totalProblems: totalProblems, - solvedCount: solvedCount, - categoryProgress: computeCategoryProgress(category, solvedProblems), - }; + if (!solvedProblems.has(problem.slug)) { + // Found first unsolved problem + // Find original index if sorted + let originalProbIdx = probIdx; + if (sortByDifficulty) { + originalProbIdx = category.problems.findIndex(p => p.slug === problem.slug); + } + + currentCategoryIndex = catIdx; + currentProblemIndex = originalProbIdx; + currentProblemSlug = problem.slug; + + // Save position for the selected set only + await saveState(catIdx, originalProbIdx, solvedProblems, selectedProblemSet); + + const totalProblems = problemSet.categories.reduce( + (sum, cat) => sum + cat.problems.length, + 0 + ); + + // Count only solved problems that are in the current problem set + const solvedCount = problemSet.categories.reduce((count, cat) => { + return count + cat.problems.filter(p => solvedProblems.has(p.slug)).length; + }, 0); + + return { + categoryIndex: catIdx, + categoryName: category.name, + problemIndex: originalProbIdx, + problem: problem, + totalProblems: totalProblems, + solvedCount: solvedCount, + categoryProgress: computeCategoryProgress(category, solvedProblems), + }; + } } } } diff --git a/src/background/redirects.js b/src/background/redirects.js index 0a6df95..f648af9 100644 --- a/src/background/redirects.js +++ b/src/background/redirects.js @@ -4,7 +4,7 @@ // Handles declarativeNetRequest rules, bypass management, and daily reset // ============================================================================ -import { WHITELIST, REDIRECT_RULE_ID, BYPASS_DURATION_MS, COOLDOWN_DURATION_MS } from '../shared/constants.js'; +import { getExclusionList, REDIRECT_RULE_ID, BYPASS_DURATION_MS, COOLDOWN_DURATION_MS } from '../shared/constants.js'; import { getDailySolveState, clearDailySolve, getBypassState, setBypassState } from './storage.js'; import { loadProblemSet, getProblemSet } from './problemLogic.js'; import { getState } from './storage.js'; @@ -43,6 +43,9 @@ export async function installRedirectRule() { const targetUrl = `https://leetcode.com/problems/${problem.slug}/`; + // Get exclusion list from storage (with fallback to defaults) + const exclusionList = await getExclusionList(); + const rule = { id: REDIRECT_RULE_ID, priority: 1, @@ -53,7 +56,7 @@ export async function installRedirectRule() { condition: { urlFilter: "|http", resourceTypes: ["main_frame"], - excludedRequestDomains: WHITELIST, + excludedRequestDomains: exclusionList, }, }; diff --git a/src/background/storage.js b/src/background/storage.js index eabf757..8b3aab7 100644 --- a/src/background/storage.js +++ b/src/background/storage.js @@ -232,3 +232,46 @@ export async function resetAllPositions() { await chrome.storage.sync.set({ positions }); } +/** + * Get the date when a problem was first opened today + * @param {string} problemSlug - Problem slug + * @returns {Promise} Date string (YYYY-MM-DD) or null if not opened today + */ +export async function getProblemFirstOpenDate(problemSlug) { + const key = `problemFirstOpened_${problemSlug}`; + const result = await chrome.storage.local.get([key]); + const storedDate = result[key]; + + if (!storedDate) { + return null; + } + + const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD + + // Return the date if it's today, otherwise null + return storedDate === today ? storedDate : null; +} + +/** + * Check if this is the first time opening a problem today + * @param {string} problemSlug - Problem slug + * @returns {Promise} True if this is the first open today + */ +export async function isFirstOpenToday(problemSlug) { + const firstOpenDate = await getProblemFirstOpenDate(problemSlug); + return firstOpenDate === null; +} + +/** + * Mark a problem as opened for today + * @param {string} problemSlug - Problem slug + * @returns {Promise} + */ +export async function markProblemFirstOpened(problemSlug) { + const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD + const key = `problemFirstOpened_${problemSlug}`; + + await chrome.storage.local.set({ [key]: today }); + console.log(`Problem ${problemSlug} marked as first opened on ${today}`); +} + diff --git a/src/content/api.js b/src/content/api.js index a24a84a..d29ae42 100644 --- a/src/content/api.js +++ b/src/content/api.js @@ -258,3 +258,4 @@ export function isSolvedToday(timestamp) { ); } + diff --git a/src/content/detector.js b/src/content/detector.js index 5314380..83dcd38 100644 --- a/src/content/detector.js +++ b/src/content/detector.js @@ -8,6 +8,37 @@ import { getCurrentSlug, resolveAlias, queryProblemStatus, getCurrentUsername, queryRecentSubmissions, queryProblemSubmissions, isSolvedToday } from './api.js'; import { showSolvedNotification } from './ui.js'; +/** + * Check if we should skip solve detection for the current problem + * Returns true if this problem was already solved today + * @returns {Promise} + */ +export async function shouldSkipSolveCheck() { + try { + const slug = getCurrentSlug(); + if (!slug) { + return false; // Not on a problem page, don't skip + } + + const canonicalSlug = resolveAlias(slug); + + // Check if this problem was already solved today + const dailyState = await chrome.storage.local.get(['dailySolveDate', 'dailySolveProblem']); + const today = new Date().toISOString().split("T")[0]; + + // If daily solve was today and it's the same problem, skip check + if (dailyState.dailySolveDate === today && + dailyState.dailySolveProblem === canonicalSlug) { + return true; + } + + return false; + } catch (error) { + console.error("Error checking if should skip:", error); + return false; // On error, don't skip (fail open) + } +} + /** * Safe message sending with context validation * @param {Object} message - Message object to send to background script @@ -40,6 +71,11 @@ export async function sendMessageSafely(message) { * and is the expected problem in the sequence */ export async function checkAndNotify() { + // Early return if this problem was already solved today + if (await shouldSkipSolveCheck()) { + return; // Already solved today, skip check + } + const slug = getCurrentSlug(); if (!slug) { console.log("No problem slug found in URL"); @@ -163,3 +199,4 @@ export async function checkAndNotify() { } } + diff --git a/src/content/editor.js b/src/content/editor.js new file mode 100644 index 0000000..7ba6c47 --- /dev/null +++ b/src/content/editor.js @@ -0,0 +1,62 @@ +// ============================================================================ +// LEETCODE BUDDY - EDITOR MODULE +// ============================================================================ +// Handles Monaco editor interactions for clearing editor content +// ============================================================================ + +/** + * Find the Monaco editor view-lines container + * @returns {HTMLElement|null} The view-lines container or null if not found + */ +function findEditorContainer() { + // Monaco editor displays content in a div with class "view-lines monaco-mouse-cursor-text" + const container = document.querySelector('.view-lines.monaco-mouse-cursor-text'); + return container; +} + +/** + * Wait for Monaco editor DOM to be available + * @param {number} maxWaitMs - Maximum time to wait in milliseconds (default: 10000) + * @param {number} pollIntervalMs - Polling interval in milliseconds (default: 200) + * @returns {Promise} Editor container element or null if timeout + */ +export async function waitForEditor(maxWaitMs = 10000, pollIntervalMs = 200) { + const startTime = Date.now(); + + while (Date.now() - startTime < maxWaitMs) { + const container = findEditorContainer(); + if (container) { + return container; + } + + await new Promise(resolve => setTimeout(resolve, pollIntervalMs)); + } + + return null; +} + +/** + * Clear the Monaco editor content by removing child divs + * @returns {Promise} True if editor was cleared successfully + */ +export async function clearEditor() { + try { + // Wait for editor DOM to be available + const container = await waitForEditor(); + + if (!container) { + console.warn("Monaco editor container not found after waiting"); + return false; + } + + // Clear all child divs (view-line elements) + // Monaco will automatically re-render with empty content + container.innerHTML = ''; + console.log("✓ Editor cleared successfully"); + return true; + } catch (error) { + console.error("Error clearing editor:", error); + return false; + } +} + diff --git a/src/content/index.js b/src/content/index.js index c1cce0f..90df551 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -5,9 +5,6 @@ // Orchestrates detection, verification, and celebration modules // ============================================================================ -import { loadAliases } from './api.js'; -import { checkAndNotify } from './detector.js'; - console.log("Leetcode Buddy - Content Script Loading"); // State @@ -15,6 +12,10 @@ let lastPathname = window.location.pathname; let checkTimeout = null; let hasCheckedOnLoad = false; +// Store imported functions +let checkAndNotify = null; +let shouldSkipSolveCheck = null; + // Watch for DOM changes that indicate a submission result const observer = new MutationObserver((mutations) => { // Look for success indicators in the DOM @@ -33,8 +34,17 @@ const observer = new MutationObserver((mutations) => { } // Check status after a short delay to let LeetCode update the backend - checkTimeout = setTimeout(() => { - checkAndNotify(); + checkTimeout = setTimeout(async () => { + if (checkAndNotify && shouldSkipSolveCheck) { + // Check guard before calling + const skip = await shouldSkipSolveCheck(); + if (!skip) { + checkAndNotify(); + } + } else if (checkAndNotify) { + // Fallback if guard not yet loaded + checkAndNotify(); + } }, 2000); break; @@ -47,9 +57,69 @@ const observer = new MutationObserver((mutations) => { try { console.log("🤝 Leetcode Buddy initializing..."); + // Use dynamic imports - files must be in web_accessible_resources + // Chrome extensions support dynamic imports for ES modules in content scripts + const apiModule = await import(chrome.runtime.getURL('src/content/api.js')); + const detectorModule = await import(chrome.runtime.getURL('src/content/detector.js')); + const editorModule = await import(chrome.runtime.getURL('src/content/editor.js')); + + // Extract functions + const { loadAliases, getCurrentSlug } = apiModule; + checkAndNotify = detectorModule.checkAndNotify; + shouldSkipSolveCheck = detectorModule.shouldSkipSolveCheck; + const { clearEditor } = editorModule; + // Load problem aliases await loadAliases(); + // Function to handle editor clearing on first open + async function handleEditorClearing() { + try { + // Check if setting is enabled + const syncResult = await chrome.storage.sync.get(['clearEditorOnFirstOpen']); + const isEnabled = syncResult.clearEditorOnFirstOpen === true; + + if (!isEnabled) { + return; // Setting disabled, skip + } + + // Get current problem slug + const slug = getCurrentSlug(); + if (!slug) { + return; // Not on a problem page + } + + // Check if this is the first open today + const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD + const key = `problemFirstOpened_${slug}`; + const localResult = await chrome.storage.local.get([key]); + const storedDate = localResult[key]; + + const isFirstOpen = storedDate !== today; + + if (isFirstOpen) { + console.log(`First time opening ${slug} today, clearing editor...`); + + // Mark as opened immediately to prevent multiple attempts + await chrome.storage.local.set({ [key]: today }); + + // Clear the editor (with delay to ensure editor is loaded) + setTimeout(async () => { + const cleared = await clearEditor(); + if (cleared) { + console.log(`Editor cleared successfully for ${slug}`); + } else { + console.warn(`Editor clearing failed for ${slug}, but problem marked as opened`); + } + }, 2000); // Wait 2 seconds for editor to initialize + } else { + console.log(`Problem ${slug} already opened today, preserving editor content`); + } + } catch (error) { + console.error("Error in editor clearing handler:", error); + } + } + // Start observing the document for changes observer.observe(document.body, { childList: true, @@ -64,11 +134,23 @@ const observer = new MutationObserver((mutations) => { lastPathname = window.location.pathname; hasCheckedOnLoad = false; + // Handle editor clearing on problem change + handleEditorClearing(); + // Check after a delay to let the page load - setTimeout(() => { - if (!hasCheckedOnLoad) { - checkAndNotify(); - hasCheckedOnLoad = true; + setTimeout(async () => { + if (!hasCheckedOnLoad && checkAndNotify) { + if (shouldSkipSolveCheck) { + const skip = await shouldSkipSolveCheck(); + if (!skip) { + checkAndNotify(); + hasCheckedOnLoad = true; + } + } else { + // Fallback if guard not yet loaded + checkAndNotify(); + hasCheckedOnLoad = true; + } } }, 3000); } @@ -78,12 +160,25 @@ const observer = new MutationObserver((mutations) => { }, 1000); // Initial check when content script loads - setTimeout(() => { + setTimeout(async () => { try { - if (!hasCheckedOnLoad) { - console.log("Initial problem status check..."); - checkAndNotify(); - hasCheckedOnLoad = true; + // Handle editor clearing on initial load + handleEditorClearing(); + + if (!hasCheckedOnLoad && checkAndNotify) { + if (shouldSkipSolveCheck) { + const skip = await shouldSkipSolveCheck(); + if (!skip) { + console.log("Initial problem status check..."); + checkAndNotify(); + hasCheckedOnLoad = true; + } + } else { + // Fallback if guard not yet loaded + console.log("Initial problem status check..."); + checkAndNotify(); + hasCheckedOnLoad = true; + } } } catch (error) { console.error("Error in initial check:", error); @@ -93,9 +188,20 @@ const observer = new MutationObserver((mutations) => { // Listen for visibility changes (tab becomes active) document.addEventListener("visibilitychange", () => { try { - if (!document.hidden) { - console.log("Tab became visible, checking status..."); - setTimeout(checkAndNotify, 1000); + if (!document.hidden && checkAndNotify) { + setTimeout(async () => { + if (shouldSkipSolveCheck) { + const skip = await shouldSkipSolveCheck(); + if (!skip) { + console.log("Tab became visible, checking status..."); + checkAndNotify(); + } + } else { + // Fallback if guard not yet loaded + console.log("Tab became visible, checking status..."); + checkAndNotify(); + } + }, 1000); } } catch (error) { console.error("Error in visibility change handler:", error); diff --git a/src/content/ui.js b/src/content/ui.js index 963ad92..2434995 100644 --- a/src/content/ui.js +++ b/src/content/ui.js @@ -49,6 +49,12 @@ export async function markCelebrationAsShown() { * Trigger confetti animation (CSS-based, no external dependencies) */ export function triggerConfetti() { + // Prevent duplicate confetti if one is already animating + if (document.getElementById('leetcode-buddy-confetti')) { + console.log("Confetti already animating, skipping duplicate"); + return; + } + const confettiContainer = document.createElement('div'); confettiContainer.id = 'leetcode-buddy-confetti'; confettiContainer.style.cssText = ` @@ -136,10 +142,10 @@ export function triggerConfetti() { } /** - * Show a celebration notification when problem is solved + * Show a celebration when problem is solved (confetti only, no toast) * @returns {Promise} */ -export async function showSolvedNotification() { +export async function showCelebration() { // Check if celebration should be shown const shouldShowCelebration = await checkIfShouldShowCelebration(); @@ -148,68 +154,14 @@ export async function showSolvedNotification() { triggerConfetti(); await markCelebrationAsShown(); } - - // Always show the notification - const notification = document.createElement("div"); - notification.id = "leetcode-buddy-notification"; - notification.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - background: linear-gradient(135deg, #10b981 0%, #059669 100%); - color: white; - padding: 20px 24px; - border-radius: 12px; - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); - z-index: 10002; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - font-size: 16px; - font-weight: 600; - max-width: 350px; - animation: slideIn 0.5s ease-out; - `; - notification.innerHTML = ` -
🎉
-
Amazing! Daily Problem Solved!
-
- All websites unblocked until midnight. Great work! 🎊 -
- `; - - // Add animations if not already added - if (!document.getElementById('notification-animation-styles')) { - const style = document.createElement('style'); - style.id = 'notification-animation-styles'; - style.textContent = ` - @keyframes slideIn { - from { - transform: translateX(400px); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } - } - @keyframes bounce { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.3); } - } - `; - document.head.appendChild(style); - } - - document.body.appendChild(notification); +} - // Remove notification after 6 seconds - setTimeout(() => { - notification.style.transition = "opacity 0.5s, transform 0.5s"; - notification.style.opacity = "0"; - notification.style.transform = "translateX(400px)"; - - setTimeout(() => { - notification.remove(); - }, 500); - }, 6000); +/** + * @deprecated Use showCelebration() instead. Kept for backward compatibility. + * Show a celebration notification when problem is solved + * @returns {Promise} + */ +export async function showSolvedNotification() { + return showCelebration(); } diff --git a/src/shared/constants.js b/src/shared/constants.js index 103ef40..967318b 100644 --- a/src/shared/constants.js +++ b/src/shared/constants.js @@ -4,16 +4,55 @@ // Shared constants used across background, content, popup, and options scripts // ============================================================================ -// Whitelist domains (websites that won't be redirected) -export const WHITELIST = [ +// System-enforced domains (always excluded, not user-editable) +export const SYSTEM_EXCLUSION_LIST = [ "leetcode.com", "neetcode.io", - "chatgpt.com", - "accounts.google.com", // Google OAuth - "github.com", // GitHub OAuth - "www.linkedin.com" // LinkedIn OAuth + "accounts.google.com" // Google OAuth ]; +// Default user-editable exclusion list (examples pre-populated) +export const DEFAULT_USER_EXCLUSION_LIST = [ + "github.com", + "linkedin.com" +]; + +// Legacy export for backward compatibility (deprecated - use getExclusionList() instead) +export const WHITELIST = [...SYSTEM_EXCLUSION_LIST, ...DEFAULT_USER_EXCLUSION_LIST]; +export const DEFAULT_EXCLUSION_LIST = WHITELIST; + +/** + * Get the complete exclusion list (system + user domains) + * @returns {Promise>} Array of domain strings + */ +export async function getExclusionList() { + try { + const result = await chrome.storage.sync.get(['userExclusionList']); + let userExclusionList = result.userExclusionList; + + // Validate that userExclusionList is an array with valid domains + if (!Array.isArray(userExclusionList) || userExclusionList.length === 0) { + // Initialize with defaults if empty + userExclusionList = [...DEFAULT_USER_EXCLUSION_LIST]; + } else { + // Basic validation: all items should be non-empty strings + const isValid = userExclusionList.every(domain => + typeof domain === 'string' && domain.trim().length > 0 + ); + + if (!isValid) { + userExclusionList = [...DEFAULT_USER_EXCLUSION_LIST]; + } + } + + // Combine system and user exclusion lists + return [...SYSTEM_EXCLUSION_LIST, ...userExclusionList]; + } catch (error) { + console.error("Failed to get exclusion list from storage:", error); + return [...SYSTEM_EXCLUSION_LIST, ...DEFAULT_USER_EXCLUSION_LIST]; + } +} + // Redirect rule configuration export const REDIRECT_RULE_ID = 1000; diff --git a/tests/background/messageHandler.test.js b/tests/background/messageHandler.test.js index d6ea310..6195399 100644 --- a/tests/background/messageHandler.test.js +++ b/tests/background/messageHandler.test.js @@ -134,6 +134,117 @@ describe('messageHandler.js', () => { const response = sendResponse.mock.calls[0][0]; expect(response.dailySolved).toBe(false); }); + + it('should add solved problem to solvedProblems list in storage', async () => { + const message = { + type: 'PROBLEM_SOLVED', + slug: 'two-sum', + timestamp: Math.floor(Date.now() / 1000), + verifiedToday: true + }; + + // Mock initial state with empty solvedProblems + chrome.storage.sync.get.mockResolvedValue({ + solvedProblems: [], + selectedProblemSet: 'neetcode250', + positions: { + neetcode250: { categoryIndex: 0, problemIndex: 0 } + } + }); + + const sendResponse = jest.fn(); + + // Mock problem set load + global.fetch.mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ + categories: [{ + name: 'Arrays & Hashing', + problems: [ + { slug: 'two-sum', leetcodeId: 1, title: 'Two Sum', difficulty: 'Easy' }, + { slug: 'valid-anagram', leetcodeId: 242, title: 'Valid Anagram', difficulty: 'Easy' } + ] + }] + }) + }); + + // Mock LeetCode API for fetchAllProblemStatuses (returns empty, no problems solved yet) + global.fetch.mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValue({ + stat_status_pairs: [] + }) + }); + + await messageHandler.handleMessage(message, {}, sendResponse); + + // Verify solvedProblems was updated in storage + expect(chrome.storage.sync.set).toHaveBeenCalledWith( + expect.objectContaining({ + solvedProblems: expect.arrayContaining(['two-sum']) + }) + ); + + // Verify response indicates success + const response = sendResponse.mock.calls[0][0]; + expect(response.success).toBe(true); + expect(response.dailySolved).toBe(true); + }); + + it('should add solved problem to existing solvedProblems list without duplicates', async () => { + const message = { + type: 'PROBLEM_SOLVED', + slug: 'valid-anagram', + timestamp: Math.floor(Date.now() / 1000), + verifiedToday: true + }; + + // Mock initial state with one existing solved problem + chrome.storage.sync.get.mockResolvedValue({ + solvedProblems: ['two-sum'], + selectedProblemSet: 'neetcode250', + positions: { + neetcode250: { categoryIndex: 0, problemIndex: 1 } + } + }); + + const sendResponse = jest.fn(); + + // Mock problem set load + global.fetch.mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ + categories: [{ + name: 'Arrays & Hashing', + problems: [ + { slug: 'two-sum', leetcodeId: 1, title: 'Two Sum', difficulty: 'Easy' }, + { slug: 'valid-anagram', leetcodeId: 242, title: 'Valid Anagram', difficulty: 'Easy' } + ] + }] + }) + }); + + // Mock LeetCode API for fetchAllProblemStatuses + global.fetch.mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValue({ + stat_status_pairs: [] + }) + }); + + await messageHandler.handleMessage(message, {}, sendResponse); + + // Verify solvedProblems contains both problems + expect(chrome.storage.sync.set).toHaveBeenCalledWith( + expect.objectContaining({ + solvedProblems: expect.arrayContaining(['two-sum', 'valid-anagram']) + }) + ); + + // Verify no duplicates (should have exactly 2 items) + const setCall = chrome.storage.sync.set.mock.calls.find(call => + call[0].solvedProblems + ); + expect(setCall[0].solvedProblems).toHaveLength(2); + }); }); describe('handleMessage - GET_STATUS', () => { diff --git a/tests/background/redirects.test.js b/tests/background/redirects.test.js index 56dab82..0506442 100644 --- a/tests/background/redirects.test.js +++ b/tests/background/redirects.test.js @@ -25,7 +25,12 @@ describe('redirects.js', () => { chrome.storage.sync.get.mockResolvedValue({ currentCategoryIndex: 0, currentProblemIndex: 0, - solvedProblems: [] + solvedProblems: [], + selectedProblemSet: 'neetcode250', + userExclusionList: ['github.com', 'linkedin.com'], + positions: { + neetcode250: { categoryIndex: 0, problemIndex: 0 } + } }); chrome.declarativeNetRequest.updateDynamicRules.mockResolvedValue(); @@ -57,14 +62,27 @@ describe('redirects.js', () => { }); it('should exclude whitelisted domains from redirect', async () => { + // Mock user exclusion list (defaults: github.com, linkedin.com) + chrome.storage.sync.get.mockResolvedValue({ + currentCategoryIndex: 0, + currentProblemIndex: 0, + solvedProblems: [], + userExclusionList: ['github.com', 'linkedin.com'] + }); + await redirects.installRedirectRule(); const call = chrome.declarativeNetRequest.updateDynamicRules.mock.calls[0][0]; const rule = call.addRules[0]; + // System domains (always excluded) expect(rule.condition.excludedRequestDomains).toContain('leetcode.com'); expect(rule.condition.excludedRequestDomains).toContain('neetcode.io'); - expect(rule.condition.excludedRequestDomains).toContain('chatgpt.com'); + expect(rule.condition.excludedRequestDomains).toContain('accounts.google.com'); + + // User domains (from storage) + expect(rule.condition.excludedRequestDomains).toContain('github.com'); + expect(rule.condition.excludedRequestDomains).toContain('linkedin.com'); }); it('should redirect to current problem URL', async () => { diff --git a/tests/content/ui.test.js b/tests/content/ui.test.js index 29427f6..09ea7b8 100644 --- a/tests/content/ui.test.js +++ b/tests/content/ui.test.js @@ -141,43 +141,14 @@ describe('ui.js', () => { }); }); - describe('showSolvedNotification', () => { - it('should create notification banner', async () => { - chrome.storage.sync.get.mockResolvedValue({ - celebrationEnabled: false // Disable celebration to test notification only - }); - - await ui.showSolvedNotification(); - - // Find notification by text content - const notifications = Array.from(document.querySelectorAll('div')).filter( - div => div.textContent?.includes('Amazing! Daily Problem Solved!') - ); - expect(notifications.length).toBeGreaterThan(0); - }); - - it('should display success message', async () => { - chrome.storage.sync.get.mockResolvedValue({ - celebrationEnabled: false - }); - - await ui.showSolvedNotification(); - - // Find notification by text content - const notifications = Array.from(document.querySelectorAll('div')).filter( - div => div.textContent?.includes('Amazing! Daily Problem Solved!') - ); - expect(notifications.length).toBeGreaterThan(0); - expect(notifications[0].textContent).toContain('Amazing! Daily Problem Solved!'); - }); - + describe('showCelebration', () => { it('should trigger confetti when enabled and not shown today', async () => { chrome.storage.sync.get.mockResolvedValue({ celebrationEnabled: true }); chrome.storage.local.get.mockResolvedValue({}); - await ui.showSolvedNotification(); + await ui.showCelebration(); const confetti = document.getElementById('leetcode-buddy-confetti'); expect(confetti).toBeTruthy(); @@ -188,9 +159,9 @@ describe('ui.js', () => { celebrationEnabled: false }); - await ui.showSolvedNotification(); + await ui.showCelebration(); - const confetti = document.querySelector('.leetcode-buddy-confetti'); + const confetti = document.getElementById('leetcode-buddy-confetti'); expect(confetti).toBeFalsy(); }); @@ -200,7 +171,7 @@ describe('ui.js', () => { }); chrome.storage.local.get.mockResolvedValue({}); - await ui.showSolvedNotification(); + await ui.showCelebration(); expect(chrome.storage.local.set).toHaveBeenCalledWith( expect.objectContaining({ @@ -209,64 +180,46 @@ describe('ui.js', () => { ); }); - it('should auto-remove notification after delay', async () => { + it('should not trigger confetti if already shown today', async () => { + const today = new Date().toISOString().split('T')[0]; chrome.storage.sync.get.mockResolvedValue({ - celebrationEnabled: false + celebrationEnabled: true + }); + chrome.storage.local.get.mockResolvedValue({ + celebrationShownDate: today }); - await ui.showSolvedNotification(); - const notification = document.getElementById('leetcode-buddy-notification'); - expect(notification).toBeTruthy(); - - // Check after 7 seconds (6s delay + 0.5s fade) - await new Promise(resolve => setTimeout(resolve, 7000)); + await ui.showCelebration(); - const removedNotification = document.getElementById('leetcode-buddy-notification'); - expect(removedNotification).toBeFalsy(); - }, 8000); + const confetti = document.getElementById('leetcode-buddy-confetti'); + expect(confetti).toBeFalsy(); + }); + }); - it('should style notification correctly', async () => { + describe('showSolvedNotification (deprecated)', () => { + it('should call showCelebration for backward compatibility', async () => { chrome.storage.sync.get.mockResolvedValue({ - celebrationEnabled: false + celebrationEnabled: true }); + chrome.storage.local.get.mockResolvedValue({}); await ui.showSolvedNotification(); - // Find notification by checking all divs (it doesn't have a class) - const notifications = Array.from(document.querySelectorAll('div')).filter( - div => div.textContent?.includes('Amazing! Daily Problem Solved!') - ); - expect(notifications.length).toBeGreaterThan(0); - - const notification = notifications[0]; - expect(notification.style.position).toBe('fixed'); - expect(notification.style.zIndex).toBeTruthy(); + // Should still trigger confetti (via showCelebration) + const confetti = document.getElementById('leetcode-buddy-confetti'); + expect(confetti).toBeTruthy(); }); }); describe('DOM Cleanup', () => { it('should not create duplicate confetti containers', () => { ui.triggerConfetti(); + // Second call should be prevented by guard ui.triggerConfetti(); - // Should still only have elements from first call (second one should clean up first) + // Should only have one container (second call should be skipped) const containers = Array.from(document.querySelectorAll('#leetcode-buddy-confetti')); - expect(containers.length).toBeLessThanOrEqual(2); // Allow some overlap during animation - }); - - it('should not create duplicate notification banners', async () => { - chrome.storage.sync.get.mockResolvedValue({ - celebrationEnabled: false - }); - - await ui.showSolvedNotification(); - await ui.showSolvedNotification(); - - // Find notifications by ID (each call creates a new notification with same ID) - // The implementation may remove the old one or keep both during animation - const notifications = Array.from(document.querySelectorAll('#leetcode-buddy-notification')); - // Allow up to 2 since there might be overlap during animation - expect(notifications.length).toBeLessThanOrEqual(2); + expect(containers.length).toBe(1); }); }); });