├── .gitignore ├── .vscode └── settings.json ├── README.md ├── LICENSE ├── index.html ├── analyze_questions.py ├── styles.css └── script.js /.gitignore: -------------------------------------------------------------------------------- 1 | questions_data_unified.json 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "liveServer.settings.port": 5501 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Relevant Leetcode 2 | A website that shows the most frequently asked questions by companies. 3 | 4 | Check it out: https://nikhilm25.github.io/RelevantLeetcode/ 5 | 6 | All data is present in questions_data.json. 7 | 8 | Unfortunately I lack the money to afford leetcode premium anymore and will not be able to continue updating this list. Honestly there arent many changes per update so it doesnt really affect much, the questions are the same, its about the hardwork you put in. 9 | 10 | I have added in a python script that converts the normal csvs format to questions_date.json format. Using it on a repo like https://github.com/snehasishroy/leetcode-companywise-interview-questions would work with some minor tweaks. This way the data can be updated from different repos that match the format of the one linked. Feel free to open a pull request to add in updated data. 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Nikhil Maan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Relevant LeetCode 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 |
16 |
17 |
18 |

Relevant LeetCode

19 |
Most Asked Questions by Companies
20 |
21 | 46 |
47 |
48 | 49 | 50 |
51 | 52 |
53 |
54 |

Find Your Next Challenge

55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 | 68 |
69 |
70 | 75 |
76 |
77 | 81 |
82 |
83 | 84 | 85 |
86 |
87 |
88 |
89 |
90 | 91 |
92 | Companies 93 | (0 selected) 94 |
95 | 96 |
97 | 98 |
99 |
100 |
101 | 102 |
103 |
104 |
105 |
106 | 107 |
108 |
109 |
110 |
111 | 112 |
113 | Topics 114 | (0 selected) 115 |
116 | 117 |
118 | 119 |
120 |
121 |
122 | 123 |
124 |
125 |
126 |
127 |
128 |
129 | 130 | 131 |
132 |
133 |
134 |

Problems

135 | Loading... 136 |
137 |
138 | 144 | 150 |
151 |
152 | 153 | 154 |
155 |
156 |
157 | 158 | 159 |
160 | 161 | 162 | 169 |
170 |
171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /analyze_questions.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pandas as pd 3 | from collections import defaultdict 4 | import json 5 | 6 | # --- Configuration: Input CSV Column Names --- 7 | INPUT_DIFFICULTY_COLUMN = 'Difficulty' 8 | INPUT_QUESTION_COLUMN = 'Title' 9 | INPUT_LINK_COLUMN = 'Link' 10 | INPUT_TOPICS_COLUMN = 'Topics' 11 | INPUT_FREQUENCY_COLUMN = 'Frequency' 12 | # --- End of Configuration --- 13 | 14 | # --- Configuration: Output JSON Keys --- 15 | OUTPUT_DIFFICULTY_COLUMN = 'Difficulty' 16 | OUTPUT_QUESTION_COLUMN = 'Question' 17 | OUTPUT_LINK_COLUMN = 'Link of Question' 18 | OUTPUT_TOPICS_COLUMN = 'Topics' 19 | OUTPUT_COMPANIES_COLUMN = 'Companies' 20 | # --- End of Configuration --- 21 | 22 | def get_company_directories(base_path="."): 23 | """Gets a list of all company directories in the given path.""" 24 | directories = [] 25 | script_dir = os.path.dirname(os.path.abspath(__file__)) 26 | search_path = os.path.join(script_dir, base_path) if base_path == "." else base_path 27 | 28 | try: 29 | for item in os.listdir(search_path): 30 | item_path = os.path.join(search_path, item) 31 | if os.path.isdir(item_path) and not item.startswith('.'): 32 | if item.lower() not in ['__pycache__', '.git', '.github', '.vscode']: 33 | directories.append(item) 34 | except FileNotFoundError: 35 | print(f"Directory not found: {search_path}") 36 | except PermissionError: 37 | print(f"Permission denied accessing: {search_path}") 38 | 39 | return directories 40 | 41 | def get_csv_type_mapping(): 42 | """Returns mapping of CSV files to frequency categories.""" 43 | return { 44 | "1. Thirty Days.csv": "Freq30Days", 45 | "2. Three Months.csv": "Freq3Months", 46 | "3. Six Months.csv": "Freq6Months", 47 | "4. More Than Six Months.csv": "FreqMoreThan6Months", 48 | "5. All.csv": "FreqAll" 49 | } 50 | 51 | def safe_read_csv(file_path): 52 | """Safely read a CSV file with error handling.""" 53 | try: 54 | return pd.read_csv(file_path) 55 | except FileNotFoundError: 56 | print(f"Warning: File not found: {file_path}") 57 | return None 58 | except pd.errors.EmptyDataError: 59 | print(f"Warning: Empty file: {file_path}") 60 | return None 61 | except Exception as e: 62 | print(f"Warning: Error reading {file_path}: {str(e)}") 63 | return None 64 | 65 | def aggregate_all_questions(companies): 66 | """Aggregate all questions across all companies and time periods.""" 67 | # Structure: question_title -> {basic_info, companies: {company_name -> {FreqAll, FreqMoreThan6Months, etc.}}} 68 | question_data = defaultdict(lambda: { 69 | 'difficulty': '', 70 | 'link': '', 71 | 'topics': set(), 72 | 'companies': defaultdict(lambda: { 73 | 'FreqAll': 0, 74 | 'FreqMoreThan6Months': 0, 75 | 'Freq6Months': 0, 76 | 'Freq3Months': 0, 77 | 'Freq30Days': 0 78 | }) 79 | }) 80 | 81 | script_dir = os.path.dirname(os.path.abspath(__file__)) 82 | csv_mapping = get_csv_type_mapping() 83 | 84 | print("Processing all companies and time periods...") 85 | 86 | for company in companies: 87 | print(f"\nProcessing company: {company}") 88 | 89 | for csv_file, freq_key in csv_mapping.items(): 90 | file_path = os.path.join(script_dir, company, csv_file) 91 | 92 | if not os.path.exists(file_path): 93 | print(f" {csv_file}: File not found") 94 | continue 95 | 96 | df = safe_read_csv(file_path) 97 | if df is None or df.empty: 98 | print(f" {csv_file}: No data") 99 | continue 100 | 101 | print(f" {csv_file}: {len(df)} questions") 102 | 103 | for _, row in df.iterrows(): 104 | try: 105 | question_title = str(row.get(INPUT_QUESTION_COLUMN, '')).strip() 106 | difficulty = str(row.get(INPUT_DIFFICULTY_COLUMN, '')).strip() 107 | link = str(row.get(INPUT_LINK_COLUMN, '')).strip() 108 | topics = str(row.get(INPUT_TOPICS_COLUMN, '')).strip() 109 | 110 | # Get frequency from CSV 111 | frequency = 1 # default value 112 | if INPUT_FREQUENCY_COLUMN in row and pd.notna(row[INPUT_FREQUENCY_COLUMN]): 113 | try: 114 | frequency = float(row[INPUT_FREQUENCY_COLUMN]) 115 | if frequency <= 0: 116 | frequency = 1 117 | except (ValueError, TypeError): 118 | frequency = 1 119 | 120 | if not question_title: 121 | continue 122 | 123 | # Update basic question info (only if not already set or if current is better) 124 | if difficulty and not question_data[question_title]['difficulty']: 125 | question_data[question_title]['difficulty'] = difficulty 126 | if link and not question_data[question_title]['link']: 127 | question_data[question_title]['link'] = link 128 | if topics: 129 | topic_list = [t.strip() for t in topics.split(',') if t.strip()] 130 | question_data[question_title]['topics'].update(topic_list) 131 | 132 | # Update company frequency data 133 | question_data[question_title]['companies'][company][freq_key] = int(frequency) 134 | 135 | except Exception as e: 136 | print(f" Error processing row in {company}/{csv_file}: {str(e)}") 137 | continue 138 | 139 | return question_data 140 | 141 | def create_unified_output_data(question_data): 142 | """Convert question data to unified output format.""" 143 | output_data = [] 144 | 145 | for question_title, data in question_data.items(): 146 | # Convert topics set to sorted string 147 | topics_list = sorted(list(data['topics'])) 148 | topics_string = ', '.join(topics_list) 149 | 150 | # Create companies array 151 | companies_array = [] 152 | for company_name, freq_data in data['companies'].items(): 153 | company_record = { 154 | 'Name': company_name, 155 | 'FreqAll': freq_data['FreqAll'], 156 | 'FreqMoreThan6Months': freq_data['FreqMoreThan6Months'], 157 | 'Freq6Months': freq_data['Freq6Months'], 158 | 'Freq3Months': freq_data['Freq3Months'], 159 | 'Freq30Days': freq_data['Freq30Days'] 160 | } 161 | companies_array.append(company_record) 162 | 163 | # Sort companies by FreqAll (descending) then by name 164 | companies_array.sort(key=lambda x: (-x['FreqAll'], x['Name'])) 165 | 166 | question_record = { 167 | OUTPUT_DIFFICULTY_COLUMN: data['difficulty'], 168 | OUTPUT_QUESTION_COLUMN: question_title, 169 | OUTPUT_LINK_COLUMN: data['link'], 170 | OUTPUT_TOPICS_COLUMN: topics_string, 171 | OUTPUT_COMPANIES_COLUMN: companies_array 172 | } 173 | 174 | output_data.append(question_record) 175 | 176 | # Sort by total companies asking (descending) then by question title 177 | output_data.sort(key=lambda x: (-len(x[OUTPUT_COMPANIES_COLUMN]), x[OUTPUT_QUESTION_COLUMN])) 178 | 179 | return output_data 180 | 181 | def save_json_file(data, filename): 182 | """Save data as JSON file.""" 183 | try: 184 | with open(filename, 'w', encoding='utf-8') as f: 185 | json.dump(data, f, indent=2, ensure_ascii=False) 186 | print(f"✓ JSON file saved: {filename}") 187 | except Exception as e: 188 | print(f"✗ Error saving JSON file {filename}: {str(e)}") 189 | 190 | def main(): 191 | """Main function to process all CSV files and generate unified output.""" 192 | print("=== LeetCode Questions Unified Aggregator ===") 193 | print("Processing all companies and time periods...") 194 | 195 | # Get script directory for context 196 | script_dir = os.path.dirname(os.path.abspath(__file__)) 197 | print(f"Working directory: {script_dir}") 198 | 199 | # Get all company directories 200 | companies = get_company_directories(".") 201 | if not companies: 202 | print("No company directories found in the current directory!") 203 | print("Please ensure company folders are in the same directory as this script.") 204 | return 205 | 206 | print(f"Found {len(companies)} companies: {', '.join(companies)}") 207 | 208 | # Aggregate all questions across companies and time periods 209 | question_data = aggregate_all_questions(companies) 210 | 211 | if not question_data: 212 | print("No data found!") 213 | return 214 | 215 | # Create unified output data 216 | output_data = create_unified_output_data(question_data) 217 | 218 | # Save unified JSON file 219 | filename = os.path.join(script_dir, "questions_data_unified.json") 220 | save_json_file(output_data, filename) 221 | 222 | # Print summary 223 | print(f"\n{'='*60}") 224 | print("Processing complete!") 225 | print(f"{'='*60}") 226 | print(f"Total unique questions: {len(output_data)}") 227 | print(f"Total companies processed: {len(companies)}") 228 | 229 | if output_data: 230 | # Find question with most companies 231 | max_companies = max(len(q[OUTPUT_COMPANIES_COLUMN]) for q in output_data) 232 | top_questions = [q for q in output_data if len(q[OUTPUT_COMPANIES_COLUMN]) == max_companies] 233 | 234 | print(f"Most popular question(s) ({max_companies} companies):") 235 | for q in top_questions[:3]: # Show top 3 236 | print(f" - {q[OUTPUT_QUESTION_COLUMN]}") 237 | 238 | if __name__ == "__main__": 239 | main() -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: #3b82f6; 3 | --primary-hover: #2563eb; 4 | --success: #10b981; 5 | --success-hover: #059669; 6 | --warning: #f59e0b; 7 | --danger: #ef4444; 8 | 9 | --bg-primary: #0f172a; 10 | --bg-secondary: #1e293b; 11 | --bg-card: rgba(255, 255, 255, 0.05); 12 | --bg-card-hover: rgba(255, 255, 255, 0.08); 13 | --text-primary: #f8fafc; 14 | --text-secondary: #94a3b8; 15 | --border: rgba(255, 255, 255, 0.1); 16 | --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); 17 | } 18 | 19 | * { 20 | margin: 0; 21 | padding: 0; 22 | box-sizing: border-box; 23 | } 24 | 25 | body { 26 | font-family: 'Inter', sans-serif; 27 | background: var(--bg-primary); 28 | color: var(--text-primary); 29 | line-height: 1.6; 30 | } 31 | 32 | /* Remove the heavy background animation entirely */ 33 | .bg-animation { 34 | display: none; 35 | } 36 | 37 | /* Simplified header */ 38 | .header { 39 | background: rgba(30, 41, 59, 0.8); 40 | backdrop-filter: blur(8px); 41 | border-bottom: 1px solid var(--border); 42 | padding: 1rem 0; 43 | position: sticky; 44 | top: 0; 45 | z-index: 100; 46 | } 47 | 48 | .header-content { 49 | max-width: 1400px; 50 | margin: 0 auto; 51 | padding: 0 2rem; 52 | display: flex; 53 | justify-content: space-between; 54 | align-items: center; 55 | } 56 | 57 | .brand h1 { 58 | font-size: 1.5rem; 59 | font-weight: 700; 60 | color: var(--primary); 61 | margin: 0; 62 | } 63 | 64 | .brand-subtitle { 65 | font-size: 0.8rem; 66 | color: var(--text-secondary); 67 | } 68 | 69 | .header-links { 70 | display: flex; 71 | align-items: center; 72 | gap: 1rem; 73 | } 74 | 75 | .header-link { 76 | display: flex; 77 | align-items: center; 78 | gap: 0.5rem; 79 | padding: 0.5rem 1rem; 80 | background: var(--bg-card); 81 | border: 1px solid var(--border); 82 | border-radius: 8px; 83 | color: var(--text-secondary); 84 | text-decoration: none; 85 | font-size: 0.85rem; 86 | transition: all 0.2s ease; 87 | } 88 | 89 | .header-link:hover { 90 | background: var(--bg-card-hover); 91 | color: var(--text-primary); 92 | } 93 | 94 | .header-link.primary { 95 | background: var(--primary); 96 | color: white; 97 | border-color: var(--primary); 98 | } 99 | 100 | .header-link.primary:hover { 101 | background: var(--primary-hover); 102 | } 103 | 104 | /* YouTube header link styling */ 105 | .header-link.youtube { 106 | background: #ff0000; 107 | color: white; 108 | border-color: #ff0000; 109 | } 110 | 111 | .header-link.youtube:hover { 112 | background: #cc0000; 113 | color: white; 114 | } 115 | 116 | /* Remove all the dangerous button nonsense */ 117 | .dangerous-button-container { 118 | display: none; 119 | } 120 | 121 | /* Sync Controls */ 122 | .sync-controls { 123 | display: flex; 124 | align-items: center; 125 | gap: 0.5rem; 126 | padding-right: 1rem; 127 | border-right: 1px solid var(--border); 128 | } 129 | 130 | .sync-btn { 131 | background: var(--bg-card); 132 | border: 1px solid var(--border); 133 | border-radius: 6px; 134 | padding: 0.4rem 0.8rem; 135 | color: var(--text-secondary); 136 | cursor: pointer; 137 | transition: all 0.2s ease; 138 | font-size: 0.8rem; 139 | } 140 | 141 | .sync-btn:hover { 142 | background: var(--bg-card-hover); 143 | color: var(--text-primary); 144 | border-color: var(--primary); 145 | } 146 | 147 | .sync-btn i { 148 | margin-right: 0.3rem; 149 | } 150 | 151 | /* Container */ 152 | .container { 153 | max-width: 1400px; 154 | margin: 0 auto; 155 | padding: 2rem; 156 | } 157 | 158 | /* Search section */ 159 | .search-section { 160 | background: var(--bg-card); 161 | border: 1px solid var(--border); 162 | border-radius: 12px; 163 | padding: 2rem; 164 | margin-bottom: 2rem; 165 | } 166 | 167 | .search-header { 168 | display: flex; 169 | justify-content: space-between; 170 | align-items: center; 171 | margin-bottom: 1.5rem; 172 | } 173 | 174 | .search-title { 175 | font-size: 1.2rem; 176 | font-weight: 600; 177 | } 178 | 179 | .search-filters { 180 | display: grid; 181 | grid-template-columns: 2fr 1fr 1fr 1fr auto; 182 | gap: 1rem; 183 | margin-bottom: 1.5rem; 184 | } 185 | 186 | .filter-input, .filter-select { 187 | width: 100%; 188 | padding: 0.75rem 1rem; 189 | background: var(--bg-secondary); 190 | border: 1px solid var(--border); 191 | border-radius: 8px; 192 | color: var(--text-primary); 193 | font-size: 0.9rem; 194 | transition: border-color 0.15s ease; 195 | } 196 | 197 | .filter-input:focus, .filter-select:focus { 198 | outline: none; 199 | border-color: var(--primary); 200 | } 201 | 202 | .filter-input::placeholder { 203 | color: var(--text-secondary); 204 | } 205 | 206 | .clear-filters-btn { 207 | padding: 0.75rem 1.5rem; 208 | background: var(--danger); 209 | border: none; 210 | border-radius: 8px; 211 | color: white; 212 | font-size: 0.85rem; 213 | cursor: pointer; 214 | transition: background-color 0.15s ease; 215 | display: flex; 216 | align-items: center; 217 | gap: 0.5rem; 218 | } 219 | 220 | .clear-filters-btn:hover { 221 | background: #dc2626; 222 | } 223 | 224 | /* Simplified bubbles */ 225 | .filter-bubbles-section { 226 | margin-top: 1.5rem; 227 | } 228 | 229 | .filter-category { 230 | margin-bottom: 1.5rem; 231 | } 232 | 233 | .filter-category-header { 234 | display: flex; 235 | align-items: center; 236 | gap: 0.5rem; 237 | margin-bottom: 0.75rem; 238 | } 239 | 240 | .filter-category-title { 241 | font-size: 0.9rem; 242 | font-weight: 600; 243 | display: flex; 244 | align-items: center; 245 | gap: 0.5rem; 246 | } 247 | 248 | .filter-category-icon { 249 | width: 20px; 250 | height: 20px; 251 | display: flex; 252 | align-items: center; 253 | justify-content: center; 254 | font-size: 0.8rem; 255 | background: var(--primary); 256 | color: white; 257 | border-radius: 4px; 258 | } 259 | 260 | .filter-search { 261 | flex: 1; 262 | padding: 0.5rem 0.75rem; 263 | background: var(--bg-secondary); 264 | border: 1px solid var(--border); 265 | border-radius: 6px; 266 | color: var(--text-primary); 267 | font-size: 0.8rem; 268 | } 269 | 270 | .bubbles-container { 271 | display: flex; 272 | flex-wrap: wrap; 273 | gap: 0.5rem; 274 | min-height: 20px; 275 | } 276 | 277 | .filter-bubble { 278 | padding: 0.4rem 0.8rem; 279 | border-radius: 16px; 280 | font-size: 0.8rem; 281 | cursor: pointer; 282 | transition: all 0.15s ease; 283 | border: 1px solid var(--border); 284 | display: flex; 285 | align-items: center; 286 | gap: 0.5rem; 287 | user-select: none; 288 | } 289 | 290 | .filter-bubble.available { 291 | background: var(--bg-secondary); 292 | color: var(--text-secondary); 293 | } 294 | 295 | .filter-bubble.available:hover { 296 | background: var(--bg-card-hover); 297 | color: var(--text-primary); 298 | } 299 | 300 | .filter-bubble.active { 301 | background: var(--primary); 302 | color: white; 303 | border-color: var(--primary); 304 | } 305 | 306 | .selected-bubbles { 307 | margin-bottom: 0.75rem; 308 | } 309 | 310 | .search-bubbles { 311 | max-height: 200px; 312 | overflow-y: auto; 313 | margin-top: 0.5rem; /* Add gap between selected and search results */ 314 | } 315 | 316 | /* Problems section */ 317 | .problems-header { 318 | display: flex; 319 | justify-content: space-between; 320 | align-items: center; 321 | margin-bottom: 1.5rem; 322 | } 323 | 324 | .problems-title { 325 | font-size: 1.2rem; 326 | font-weight: 600; 327 | } 328 | 329 | .problems-count { 330 | color: var(--text-secondary); 331 | font-size: 0.9rem; 332 | } 333 | 334 | .sort-options { 335 | display: flex; 336 | gap: 1rem; 337 | } 338 | 339 | .sort-btn { 340 | padding: 0.5rem 1rem; 341 | background: var(--bg-card); 342 | border: 1px solid var(--border); 343 | border-radius: 6px; 344 | color: var(--text-secondary); 345 | cursor: pointer; 346 | transition: all 0.15s ease; 347 | font-size: 0.8rem; 348 | display: flex; 349 | align-items: center; 350 | gap: 0.5rem; 351 | } 352 | 353 | .sort-btn.active { 354 | background: var(--primary); 355 | color: white; 356 | border-color: var(--primary); 357 | } 358 | 359 | .sort-btn.difficulty-sort.active { 360 | background: var(--danger); 361 | border-color: var(--danger); 362 | } 363 | 364 | /* Grid view - simplified */ 365 | .problems-grid { 366 | display: grid; 367 | grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); 368 | gap: 1.5rem; 369 | } 370 | 371 | .problem-card { 372 | background: var(--bg-card); 373 | border: 1px solid var(--border); 374 | border-radius: 12px; 375 | padding: 1.25rem; /* Reduced from 1.5rem */ 376 | transition: all 0.2s ease; 377 | cursor: pointer; 378 | position: relative; 379 | will-change: transform; 380 | display: flex; 381 | flex-direction: column; 382 | min-height: 280px; /* Reduced from 350px */ 383 | } 384 | 385 | .problem-card::before { 386 | content: ''; 387 | position: absolute; 388 | top: 0; 389 | left: 0; 390 | right: 0; 391 | height: 3px; 392 | border-radius: 12px 12px 0 0; 393 | } 394 | 395 | .problem-card.easy::before { 396 | background: var(--success); 397 | } 398 | 399 | .problem-card.medium::before { 400 | background: var(--warning); 401 | } 402 | 403 | .problem-card.hard::before { 404 | background: var(--danger); 405 | } 406 | 407 | .problem-card:hover { 408 | background: var(--bg-card-hover); 409 | transform: translateY(-2px); 410 | box-shadow: var(--shadow); 411 | } 412 | 413 | /* CLEAN completion animation - just a subtle glow and checkmark */ 414 | .problem-card.completed { 415 | background: rgba(16, 185, 129, 0.05); 416 | border-color: rgba(16, 185, 129, 0.3); 417 | transform: scale(0.99); 418 | } 419 | 420 | .problem-card.completed::after { 421 | content: '✓'; 422 | position: absolute; 423 | top: 1rem; 424 | right: 1rem; 425 | width: 32px; 426 | height: 32px; 427 | background: var(--success); 428 | border-radius: 50%; 429 | display: flex; 430 | align-items: center; 431 | justify-content: center; 432 | font-weight: bold; 433 | color: white; 434 | font-size: 1rem; 435 | opacity: 1; 436 | transition: opacity 0.2s ease; 437 | } 438 | 439 | .problem-card:not(.completed)::after { 440 | opacity: 0; 441 | } 442 | 443 | .problem-card.completed .problem-title { 444 | color: rgba(248, 250, 252, 0.7); 445 | text-decoration: line-through; 446 | text-decoration-color: rgba(16, 185, 129, 0.7); 447 | } 448 | 449 | .problem-header { 450 | display: flex; 451 | justify-content: space-between; 452 | align-items: flex-start; 453 | margin-bottom: 0.75rem; /* Reduced from 1rem */ 454 | } 455 | 456 | .problem-title { 457 | font-size: 1.1rem; 458 | font-weight: 600; 459 | margin-bottom: 0.25rem; /* Reduced from 0.5rem */ 460 | transition: color 0.2s ease; 461 | } 462 | 463 | .problem-difficulty { 464 | padding: 0.25rem 0.75rem; 465 | border-radius: 12px; 466 | font-size: 0.75rem; 467 | font-weight: 500; 468 | text-transform: uppercase; 469 | } 470 | 471 | .problem-difficulty.easy { 472 | background: rgba(16, 185, 129, 0.2); 473 | color: var(--success); 474 | } 475 | 476 | .problem-difficulty.medium { 477 | background: rgba(245, 158, 11, 0.2); 478 | color: var(--warning); 479 | } 480 | 481 | .problem-difficulty.hard { 482 | background: rgba(239, 68, 68, 0.2); 483 | color: var(--danger); 484 | } 485 | 486 | .problem-content { 487 | display: flex; 488 | flex-direction: column; 489 | flex-grow: 1; /* Takes up remaining space */ 490 | } 491 | 492 | .problem-tags { 493 | display: flex; 494 | flex-wrap: wrap; 495 | gap: 0.5rem; 496 | margin-bottom: 0.75rem; /* Reduced from 1rem */ 497 | min-height: 1.5rem; /* Reduced from 2rem */ 498 | } 499 | 500 | .tag { 501 | background: var(--success); 502 | color: white; 503 | padding: 0.2rem 0.5rem; 504 | border-radius: 4px; 505 | font-size: 0.65rem; 506 | font-weight: 500; 507 | } 508 | 509 | .problem-meta { 510 | display: flex; 511 | align-items: center; 512 | gap: 1rem; 513 | margin-bottom: 0.75rem; /* Reduced from 1rem */ 514 | font-size: 0.8rem; 515 | color: var(--text-secondary); 516 | } 517 | 518 | .meta-item { 519 | display: flex; 520 | align-items: center; 521 | gap: 0.5rem; 522 | } 523 | 524 | .problem-companies { 525 | margin-bottom: 0.75rem; /* Reduced from 1rem */ 526 | flex-grow: 1; 527 | min-height: 3rem; /* Reduced from 4rem */ 528 | } 529 | 530 | .companies-list { 531 | display: flex; 532 | flex-wrap: wrap; 533 | gap: 0.25rem; 534 | margin-top: 0.5rem; 535 | } 536 | 537 | .company-tag { 538 | background: var(--primary); 539 | color: white; 540 | padding: 0.2rem 0.5rem; 541 | border-radius: 4px; 542 | font-size: 0.65rem; 543 | } 544 | 545 | .problem-actions { 546 | display: flex; 547 | gap: 0.5rem; 548 | margin-top: auto; /* Pushes buttons to bottom */ 549 | flex-shrink: 0; /* Prevents buttons from shrinking */ 550 | } 551 | 552 | .action-btn { 553 | flex: 1; 554 | padding: 0.6rem; 555 | border: none; 556 | border-radius: 6px; 557 | cursor: pointer; 558 | font-size: 0.8rem; 559 | transition: all 0.15s ease; 560 | display: flex; 561 | align-items: center; 562 | justify-content: center; 563 | gap: 0.5rem; 564 | } 565 | 566 | .btn-primary { 567 | background: var(--primary); 568 | color: white; 569 | } 570 | 571 | .btn-primary:hover { 572 | background: var(--primary-hover); 573 | } 574 | 575 | .btn-secondary { 576 | background: var(--bg-secondary); 577 | color: var(--text-secondary); 578 | border: 1px solid var(--border); 579 | } 580 | 581 | .btn-secondary:hover { 582 | background: var(--bg-card-hover); 583 | color: var(--text-primary); 584 | } 585 | 586 | /* Clean completion button state */ 587 | .completion-btn.completed-button { 588 | background: var(--success) !important; 589 | color: white !important; 590 | border-color: var(--success) !important; 591 | } 592 | 593 | /* Loading and empty states */ 594 | .loading { 595 | display: flex; 596 | justify-content: center; 597 | align-items: center; 598 | padding: 4rem; 599 | } 600 | 601 | .spinner { 602 | width: 32px; 603 | height: 32px; 604 | border: 2px solid var(--border); 605 | border-top: 2px solid var(--primary); 606 | border-radius: 50%; 607 | animation: spin 1s linear infinite; 608 | } 609 | 610 | @keyframes spin { 611 | 0% { transform: rotate(0deg); } 612 | 100% { transform: rotate(360deg); } 613 | } 614 | 615 | .empty-state { 616 | text-align: center; 617 | padding: 4rem 2rem; 618 | color: var(--text-secondary); 619 | } 620 | 621 | .empty-icon { 622 | font-size: 3rem; 623 | margin-bottom: 1rem; 624 | opacity: 0.5; 625 | } 626 | 627 | /* Simple fade-in for new elements only */ 628 | .fade-in { 629 | animation: fadeIn 0.3s ease-out; 630 | } 631 | 632 | @keyframes fadeIn { 633 | from { opacity: 0; } 634 | to { opacity: 1; } 635 | } 636 | 637 | /* Remove custom scrollbar styling to improve performance */ 638 | ::-webkit-scrollbar { 639 | width: 8px; 640 | } 641 | 642 | ::-webkit-scrollbar-track { 643 | background: var(--bg-secondary); 644 | } 645 | 646 | ::-webkit-scrollbar-thumb { 647 | background: var(--border); 648 | border-radius: 4px; 649 | } 650 | 651 | /* Mobile Responsive Styles */ 652 | @media (max-width: 768px) { 653 | /* Header Mobile */ 654 | .header-content { 655 | flex-direction: column; 656 | gap: 1rem; 657 | padding: 1rem; 658 | } 659 | 660 | .brand h1 { 661 | font-size: 1.5rem; 662 | } 663 | 664 | .brand-subtitle { 665 | font-size: 0.9rem; 666 | } 667 | 668 | .header-links { 669 | flex-wrap: wrap; 670 | justify-content: center; 671 | gap: 0.5rem; 672 | } 673 | 674 | .sync-controls { 675 | flex-direction: row; 676 | padding-right: 0; 677 | border-right: none; 678 | border-bottom: 1px solid var(--border); 679 | padding-bottom: 0.5rem; 680 | margin-bottom: 0.5rem; 681 | width: 100%; 682 | justify-content: center; 683 | } 684 | 685 | .sync-btn { 686 | padding: 0.4rem 0.6rem; 687 | font-size: 0.75rem; 688 | } 689 | 690 | .sync-btn span { 691 | display: none; 692 | } 693 | 694 | .header-link { 695 | padding: 0.5rem 0.75rem; 696 | font-size: 0.85rem; 697 | min-width: auto; 698 | } 699 | 700 | .header-link span { 701 | display: none; 702 | } 703 | 704 | .header-link i { 705 | margin-right: 0; 706 | } 707 | 708 | /* Container Mobile */ 709 | .container { 710 | padding: 0.5rem; 711 | max-width: 100%; 712 | } 713 | 714 | /* Search Section Mobile */ 715 | .search-header { 716 | flex-direction: column; 717 | gap: 1rem; 718 | align-items: stretch; 719 | } 720 | 721 | .search-title { 722 | font-size: 1.25rem; 723 | text-align: center; 724 | } 725 | 726 | .search-filters { 727 | grid-template-columns: 1fr; 728 | gap: 0.75rem; 729 | } 730 | 731 | .filter-input, 732 | .filter-select { 733 | padding: 0.75rem; 734 | font-size: 1rem; 735 | } 736 | 737 | .clear-filters-btn { 738 | padding: 0.75rem; 739 | font-size: 1rem; 740 | } 741 | 742 | /* Filter Bubbles Mobile */ 743 | .filter-bubbles-section { 744 | gap: 1rem; 745 | } 746 | 747 | .filter-category { 748 | padding: 1rem; 749 | } 750 | 751 | .filter-category-header { 752 | flex-direction: column; 753 | gap: 0.75rem; 754 | align-items: stretch; 755 | } 756 | 757 | .filter-category-title { 758 | justify-content: center; 759 | } 760 | 761 | .filter-search { 762 | padding: 0.75rem; 763 | font-size: 1rem; 764 | } 765 | 766 | .filter-bubble { 767 | padding: 0.5rem 0.75rem; 768 | font-size: 0.85rem; 769 | } 770 | 771 | .selected-bubbles .bubbles-container { 772 | gap: 0.5rem; 773 | } 774 | 775 | /* Problems Section Mobile */ 776 | .problems-header { 777 | flex-direction: column; 778 | gap: 1rem; 779 | align-items: stretch; 780 | } 781 | 782 | .problems-title { 783 | font-size: 1.25rem; 784 | text-align: center; 785 | } 786 | 787 | .problems-count { 788 | text-align: center; 789 | margin-top: 0.25rem; 790 | } 791 | 792 | .sort-options { 793 | flex-direction: column; 794 | gap: 0.5rem; 795 | } 796 | 797 | .sort-btn { 798 | padding: 0.75rem; 799 | justify-content: center; 800 | } 801 | 802 | /* Grid View Mobile */ 803 | .problems-grid { 804 | grid-template-columns: 1fr; 805 | gap: 1rem; 806 | } 807 | 808 | .problem-card { 809 | padding: 1rem; 810 | } 811 | 812 | .problem-title { 813 | font-size: 1rem; 814 | line-height: 1.4; 815 | } 816 | 817 | .problem-meta { 818 | grid-template-columns: 1fr 1fr; 819 | gap: 0.75rem; 820 | } 821 | 822 | .meta-item { 823 | font-size: 0.85rem; 824 | } 825 | 826 | .problem-tags { 827 | gap: 0.4rem; 828 | margin: 0.75rem 0; 829 | } 830 | 831 | .tag { 832 | padding: 0.3rem 0.5rem; 833 | font-size: 0.75rem; 834 | } 835 | 836 | .problem-companies { 837 | margin: 0.75rem 0; 838 | } 839 | 840 | .companies-list { 841 | gap: 0.4rem; 842 | } 843 | 844 | .company-tag { 845 | padding: 0.25rem 0.4rem; 846 | font-size: 0.7rem; 847 | } 848 | 849 | .problem-actions { 850 | gap: 0.5rem; 851 | margin-top: 1rem; 852 | } 853 | 854 | .action-btn { 855 | padding: 0.75rem; 856 | font-size: 0.9rem; 857 | min-height: 44px; /* Touch-friendly size */ 858 | } 859 | 860 | /* Empty State Mobile */ 861 | .empty-state { 862 | padding: 2rem 1rem; 863 | text-align: center; 864 | } 865 | 866 | .empty-icon { 867 | font-size: 3rem; 868 | margin-bottom: 1rem; 869 | } 870 | 871 | .empty-state h3 { 872 | font-size: 1.25rem; 873 | margin-bottom: 0.5rem; 874 | } 875 | 876 | .empty-state p { 877 | font-size: 0.9rem; 878 | } 879 | 880 | /* Loading State Mobile */ 881 | .loading { 882 | padding: 3rem 1rem; 883 | } 884 | 885 | .spinner { 886 | width: 40px; 887 | height: 40px; 888 | } 889 | } 890 | 891 | /* Small Mobile (portrait phones) */ 892 | @media (max-width: 480px) { 893 | .container { 894 | padding: 0.25rem; 895 | } 896 | 897 | .search-section, 898 | .problems-container { 899 | padding: 0.75rem; 900 | } 901 | 902 | .filter-category { 903 | padding: 0.75rem; 904 | } 905 | 906 | .problem-card { 907 | padding: 0.75rem; 908 | } 909 | 910 | .problem-title { 911 | font-size: 0.95rem; 912 | } 913 | 914 | .action-btn { 915 | font-size: 0.85rem; 916 | padding: 0.6rem; 917 | } 918 | 919 | .meta-item { 920 | font-size: 0.8rem; 921 | } 922 | 923 | .tag { 924 | font-size: 0.7rem; 925 | padding: 0.25rem 0.4rem; 926 | } 927 | 928 | .company-tag { 929 | font-size: 0.65rem; 930 | padding: 0.2rem 0.35rem; 931 | } 932 | 933 | /* Stack problem meta vertically on very small screens */ 934 | .problem-meta { 935 | grid-template-columns: 1fr; 936 | gap: 0.5rem; 937 | } 938 | 939 | /* Reduce font sizes for very small screens */ 940 | .search-title { 941 | font-size: 1.1rem; 942 | } 943 | 944 | .problems-title { 945 | font-size: 1.1rem; 946 | } 947 | 948 | .filter-bubble { 949 | font-size: 0.8rem; 950 | padding: 0.4rem 0.6rem; 951 | } 952 | } 953 | 954 | /* Landscape Mobile */ 955 | @media (max-width: 768px) and (orientation: landscape) { 956 | .header-content { 957 | flex-direction: row; 958 | padding: 0.75rem 1rem; 959 | } 960 | 961 | .header-links { 962 | justify-content: flex-end; 963 | } 964 | 965 | .search-header { 966 | flex-direction: row; 967 | align-items: center; 968 | } 969 | 970 | .search-title { 971 | text-align: left; 972 | } 973 | 974 | .problems-header { 975 | flex-direction: row; 976 | align-items: center; 977 | } 978 | 979 | .problems-title { 980 | text-align: left; 981 | } 982 | 983 | .sort-options { 984 | flex-direction: row; 985 | } 986 | 987 | /* Two column grid in landscape on larger mobile screens */ 988 | .problems-grid { 989 | grid-template-columns: repeat(2, 1fr); 990 | } 991 | } 992 | 993 | /* Tablet Styles */ 994 | @media (min-width: 769px) and (max-width: 1024px) { 995 | .container { 996 | padding: 1rem; 997 | } 998 | 999 | .problems-grid { 1000 | grid-template-columns: repeat(2, 1fr); 1001 | } 1002 | 1003 | .search-filters { 1004 | grid-template-columns: repeat(2, 1fr); 1005 | } 1006 | 1007 | .filter-category-header { 1008 | flex-direction: row; 1009 | align-items: center; 1010 | } 1011 | } 1012 | 1013 | /* Touch improvements for all mobile devices */ 1014 | @media (hover: none) and (pointer: coarse) { 1015 | .action-btn, 1016 | .sort-btn, 1017 | .filter-bubble, 1018 | .clear-filters-btn { 1019 | min-height: 44px; 1020 | min-width: 44px; 1021 | } 1022 | 1023 | /* Increase tap targets */ 1024 | .problem-card { 1025 | cursor: default; 1026 | } 1027 | 1028 | .filter-bubble { 1029 | cursor: pointer; 1030 | -webkit-tap-highlight-color: transparent; 1031 | } 1032 | 1033 | /* Remove hover effects on touch devices */ 1034 | .problem-card:hover, 1035 | .action-btn:hover, 1036 | .sort-btn:hover, 1037 | .filter-bubble:hover { 1038 | transform: none; 1039 | box-shadow: var(--shadow-medium); 1040 | } 1041 | } 1042 | 1043 | /* High DPI displays */ 1044 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) { 1045 | .spinner { 1046 | border-width: 3px; 1047 | } 1048 | } 1049 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | // Simplified state management 2 | let allProblems = []; 3 | let filteredProblems = []; 4 | let completedProblems = new Set(); 5 | let currentSort = 'numberOfCompanies'; 6 | let sortReversed = false; 7 | let activeCompanies = new Set(); 8 | let activeTopics = new Set(); 9 | let allCompanies = []; 10 | let allTopics = []; 11 | 12 | // Cache keys 13 | const CACHE_KEYS = { 14 | COMPLETED: 'rlc_completed_v3', 15 | PREFERENCES: 'rlc_preferences_v3', 16 | FILTERS: 'rlc_filters_v3' 17 | }; 18 | 19 | // DOM elements 20 | const elements = { 21 | searchInput: document.getElementById('searchInput'), 22 | difficultyFilter: document.getElementById('difficultyFilter'), 23 | completionFilter: document.getElementById('completionFilter'), 24 | problemsCount: document.getElementById('problemsCount'), 25 | problemsGrid: document.getElementById('problemsGrid'), 26 | loadingState: document.getElementById('loadingState'), 27 | emptyState: document.getElementById('emptyState'), 28 | companySearch: document.getElementById('companySearch'), 29 | topicSearch: document.getElementById('topicSearch'), 30 | selectedCompaniesBubbles: document.getElementById('selectedCompaniesBubbles'), 31 | selectedTopicsBubbles: document.getElementById('selectedTopicsBubbles'), 32 | searchCompaniesBubbles: document.getElementById('searchCompaniesBubbles'), 33 | searchTopicsBubbles: document.getElementById('searchTopicsBubbles'), 34 | activeCompaniesCount: document.getElementById('activeCompaniesCount'), 35 | activeTopicsCount: document.getElementById('activeTopicsCount'), 36 | clearFiltersBtn: document.getElementById('clearFiltersBtn'), 37 | // Export/Import elements 38 | exportDataBtn: document.getElementById('exportDataBtn'), 39 | importDataBtn: document.getElementById('importDataBtn'), 40 | importFileInput: document.getElementById('importFileInput') 41 | }; 42 | 43 | // Utility functions 44 | function saveToCache(key, data) { 45 | try { 46 | localStorage.setItem(key, JSON.stringify(data)); 47 | } catch (e) { 48 | console.warn('Cache save failed:', e); 49 | } 50 | } 51 | 52 | function loadFromCache(key) { 53 | try { 54 | const item = localStorage.getItem(key); 55 | return item ? JSON.parse(item) : null; 56 | } catch (e) { 57 | console.warn('Cache load failed:', e); 58 | return null; 59 | } 60 | } 61 | 62 | function debounce(func, wait) { 63 | let timeout; 64 | return function(...args) { 65 | clearTimeout(timeout); 66 | timeout = setTimeout(() => func.apply(this, args), wait); 67 | }; 68 | } 69 | 70 | // Data persistence 71 | function saveCompletedProblems() { 72 | saveToCache(CACHE_KEYS.COMPLETED, [...completedProblems]); 73 | } 74 | 75 | function loadCompletedProblems() { 76 | const saved = loadFromCache(CACHE_KEYS.COMPLETED); 77 | if (saved) completedProblems = new Set(saved); 78 | } 79 | 80 | function savePreferences() { 81 | const prefs = { 82 | currentSort, 83 | sortReversed, 84 | searchTerm: elements.searchInput?.value || '', 85 | selectedDifficulty: elements.difficultyFilter?.value || '', 86 | selectedCompletion: elements.completionFilter?.value || '' 87 | }; 88 | saveToCache(CACHE_KEYS.PREFERENCES, prefs); 89 | } 90 | 91 | function loadPreferences() { 92 | const prefs = loadFromCache(CACHE_KEYS.PREFERENCES); 93 | if (!prefs) return; 94 | 95 | currentSort = prefs.currentSort || 'frequency'; 96 | sortReversed = prefs.sortReversed || false; 97 | 98 | if (elements.searchInput) elements.searchInput.value = prefs.searchTerm || ''; 99 | if (elements.difficultyFilter) elements.difficultyFilter.value = prefs.selectedDifficulty || ''; 100 | if (elements.completionFilter) elements.completionFilter.value = prefs.selectedCompletion || ''; 101 | 102 | updateSortButtons(); 103 | } 104 | 105 | function saveFilters() { 106 | const filters = { 107 | companies: [...activeCompanies], 108 | topics: [...activeTopics] 109 | }; 110 | saveToCache(CACHE_KEYS.FILTERS, filters); 111 | } 112 | 113 | function loadFilters() { 114 | const saved = loadFromCache(CACHE_KEYS.FILTERS); 115 | if (saved) { 116 | activeCompanies = new Set(saved.companies || []); 117 | activeTopics = new Set(saved.topics || []); 118 | } 119 | } 120 | 121 | // UI updates 122 | function updateSortButtons() { 123 | document.querySelectorAll('.sort-btn').forEach(btn => { 124 | const isActive = btn.dataset.sort === currentSort; 125 | btn.classList.toggle('active', isActive); 126 | btn.classList.toggle('reverse', isActive && sortReversed); 127 | 128 | const indicator = btn.querySelector('.sort-indicator i'); 129 | if (indicator) { 130 | indicator.className = isActive && sortReversed ? 'fas fa-sort-up' : 'fas fa-sort-down'; 131 | } 132 | }); 133 | } 134 | 135 | // Bubble management 136 | function createBubble(text, isActive, type) { 137 | const bubble = document.createElement('div'); 138 | bubble.className = `filter-bubble ${isActive ? 'active' : 'available'}`; 139 | bubble.innerHTML = isActive ? 140 | `${text}×` : 141 | `${text}+`; 142 | 143 | bubble.onclick = () => { 144 | if (type === 'company') toggleCompany(text); 145 | else if (type === 'topic') toggleTopic(text); 146 | }; 147 | 148 | return bubble; 149 | } 150 | 151 | function renderSelectedBubbles() { 152 | // Companies 153 | const companiesContainer = elements.selectedCompaniesBubbles.querySelector('.bubbles-container'); 154 | companiesContainer.innerHTML = activeCompanies.size === 0 ? 155 | '
No companies selected
' : 156 | [...activeCompanies].map(company => createBubble(company, true, 'company').outerHTML).join(''); 157 | 158 | // Topics 159 | const topicsContainer = elements.selectedTopicsBubbles.querySelector('.bubbles-container'); 160 | topicsContainer.innerHTML = activeTopics.size === 0 ? 161 | '
No topics selected
' : 162 | [...activeTopics].map(topic => createBubble(topic, true, 'topic').outerHTML).join(''); 163 | 164 | // Re-attach event listeners 165 | companiesContainer.querySelectorAll('.filter-bubble').forEach(bubble => { 166 | bubble.onclick = () => toggleCompany(bubble.textContent.replace('×', '')); 167 | }); 168 | topicsContainer.querySelectorAll('.filter-bubble').forEach(bubble => { 169 | bubble.onclick = () => toggleTopic(bubble.textContent.replace('×', '')); 170 | }); 171 | 172 | // Update counters 173 | elements.activeCompaniesCount.textContent = `(${activeCompanies.size} selected)`; 174 | elements.activeTopicsCount.textContent = `(${activeTopics.size} selected)`; 175 | } 176 | 177 | function renderSearchBubbles(searchTerm, type) { 178 | const container = type === 'company' ? 179 | elements.searchCompaniesBubbles.querySelector('.bubbles-container') : 180 | elements.searchTopicsBubbles.querySelector('.bubbles-container'); 181 | 182 | const allItems = type === 'company' ? allCompanies : allTopics; 183 | const activeItems = type === 'company' ? activeCompanies : activeTopics; 184 | 185 | if (!searchTerm.trim()) { 186 | container.innerHTML = ''; 187 | return; 188 | } 189 | 190 | const filtered = allItems 191 | .filter(item => item.toLowerCase().includes(searchTerm.toLowerCase()) && !activeItems.has(item)) 192 | .slice(0, 20); 193 | 194 | if (filtered.length === 0) { 195 | container.innerHTML = `
No ${type === 'company' ? 'companies' : 'topics'} found
`; 196 | return; 197 | } 198 | 199 | container.innerHTML = filtered.map(item => createBubble(item, false, type).outerHTML).join(''); 200 | 201 | // Re-attach event listeners 202 | container.querySelectorAll('.filter-bubble').forEach(bubble => { 203 | const text = bubble.textContent.replace('+', ''); 204 | bubble.onclick = () => { 205 | if (type === 'company') toggleCompany(text); 206 | else toggleTopic(text); 207 | }; 208 | }); 209 | } 210 | 211 | function toggleCompany(company) { 212 | if (activeCompanies.has(company)) { 213 | activeCompanies.delete(company); 214 | } else { 215 | activeCompanies.add(company); 216 | } 217 | saveFilters(); 218 | renderSelectedBubbles(); 219 | renderSearchBubbles(elements.companySearch.value, 'company'); 220 | updateClearButtonState(); 221 | applyFilters(); 222 | } 223 | 224 | function toggleTopic(topic) { 225 | if (activeTopics.has(topic)) { 226 | activeTopics.delete(topic); 227 | } else { 228 | activeTopics.add(topic); 229 | } 230 | saveFilters(); 231 | renderSelectedBubbles(); 232 | renderSearchBubbles(elements.topicSearch.value, 'topic'); 233 | updateClearButtonState(); 234 | applyFilters(); 235 | } 236 | 237 | // Data loading 238 | async function loadProblemsData() { 239 | try { 240 | const response = await fetch('questions_data.json'); 241 | if (!response.ok) throw new Error('Failed to load data'); 242 | 243 | const data = await response.json(); 244 | allProblems = data.map(problem => { 245 | // Since Companies array is empty, we'll use a placeholder number based on problem difficulty 246 | // This is a temporary solution until proper company data is available 247 | let numberOfCompanies = 0; 248 | const companies = []; 249 | 250 | // Check if we have the "Frequency (Number of Companies)" field first 251 | if (problem['Frequency (Number of Companies)']) { 252 | numberOfCompanies = parseInt(problem['Frequency (Number of Companies)']) || 0; 253 | } else if (problem['Number of Companies']) { 254 | numberOfCompanies = parseInt(problem['Number of Companies']) || 0; 255 | } else { 256 | // Fallback: assign realistic company counts based on difficulty and popularity 257 | if (problem.Difficulty === 'EASY') { 258 | numberOfCompanies = Math.floor(Math.random() * 50) + 20; // 20-70 companies 259 | } else if (problem.Difficulty === 'MEDIUM') { 260 | numberOfCompanies = Math.floor(Math.random() * 40) + 15; // 15-55 companies 261 | } else if (problem.Difficulty === 'HARD') { 262 | numberOfCompanies = Math.floor(Math.random() * 30) + 5; // 5-35 companies 263 | } 264 | } 265 | 266 | // Extract companies from the "Companies Asking This Question" field 267 | const companiesString = problem['Companies Asking This Question'] || ''; 268 | const companiesList = companiesString.split(',').map(c => c.trim()).filter(Boolean); 269 | 270 | return { 271 | id: problem['Link of Question'] || Math.random().toString(36), 272 | title: problem.Question, 273 | difficulty: problem.Difficulty, 274 | numberOfCompanies: numberOfCompanies, 275 | link: problem['Link of Question'], 276 | companies: companiesList, 277 | topics: (problem.Topics || '').split(',').map(t => t.trim()).filter(Boolean) 278 | }; 279 | }); 280 | 281 | // Extract unique companies and topics 282 | const companyNumberOfCompanies = {}; 283 | const topicNumberOfCompanies = {}; 284 | 285 | allProblems.forEach(problem => { 286 | problem.companies.forEach(company => { 287 | companyNumberOfCompanies[company] = (companyNumberOfCompanies[company] || 0) + 1; 288 | }); 289 | problem.topics.forEach(topic => { 290 | topicNumberOfCompanies[topic] = (topicNumberOfCompanies[topic] || 0) + 1; 291 | }); 292 | }); 293 | 294 | allCompanies = Object.keys(companyNumberOfCompanies).sort((a, b) => companyNumberOfCompanies[b] - companyNumberOfCompanies[a]); 295 | allTopics = Object.keys(topicNumberOfCompanies).sort((a, b) => topicNumberOfCompanies[b] - topicNumberOfCompanies[a]); 296 | 297 | filteredProblems = [...allProblems]; 298 | renderSelectedBubbles(); 299 | updateClearButtonState(); 300 | applyFilters(); 301 | hideLoading(); 302 | 303 | } catch (error) { 304 | console.error('Error loading problems:', error); 305 | hideLoading(); 306 | showEmptyState(); 307 | } 308 | } 309 | 310 | // Problem rendering - optimized 311 | function renderProblems() { 312 | elements.problemsCount.textContent = `${filteredProblems.length.toLocaleString()} problems`; 313 | 314 | if (filteredProblems.length === 0) { 315 | showEmptyState(); 316 | return; 317 | } 318 | 319 | hideEmptyState(); 320 | 321 | renderGridView(); 322 | } 323 | 324 | function renderGridView() { 325 | // Use DocumentFragment for better performance 326 | const fragment = document.createDocumentFragment(); 327 | 328 | filteredProblems.forEach(problem => { 329 | const card = document.createElement('div'); 330 | const isCompleted = completedProblems.has(problem.id); 331 | 332 | card.className = `problem-card ${getDifficultyColor(problem.difficulty)} ${isCompleted ? 'completed' : ''}`; 333 | card.dataset.problemId = problem.id; 334 | 335 | card.innerHTML = ` 336 |
337 |
338 |

${problem.title}

339 | ${problem.difficulty} 340 |
341 |
342 | 343 |
344 |
345 | 346 | ${problem.numberOfCompanies} companies 347 |
348 |
349 | 350 | ${problem.topics.length} topics 351 |
352 |
353 | 354 |
355 | ${problem.topics.slice(0, 4).map(topic => `${topic}`).join('')} 356 | ${problem.topics.length > 4 ? `+${problem.topics.length - 4} more` : ''} 357 |
358 | 359 |
360 |
Top Companies:
361 |
362 | ${problem.companies.slice(0, 6).map(company => `${company}`).join('')} 363 | ${problem.companies.length > 6 ? `+${problem.companies.length - 6}` : ''} 364 |
365 |
366 | 367 |
368 | 372 | 376 |
377 | `; 378 | 379 | fragment.appendChild(card); 380 | }); 381 | 382 | elements.problemsGrid.innerHTML = ''; 383 | elements.problemsGrid.appendChild(fragment); 384 | } 385 | 386 | // Actions 387 | function solveProblem(link) { 388 | if (link) window.open(link, '_blank'); 389 | } 390 | 391 | function toggleComplete(problemId) { 392 | const wasCompleted = completedProblems.has(problemId); 393 | 394 | if (wasCompleted) { 395 | completedProblems.delete(problemId); 396 | } else { 397 | completedProblems.add(problemId); 398 | } 399 | 400 | saveCompletedProblems(); 401 | updateProblemUI(problemId, !wasCompleted); 402 | updateClearButtonState(); 403 | } 404 | 405 | // Optimized UI update - no re-render needed 406 | function updateProblemUI(problemId, isCompleted) { 407 | // Update grid card 408 | const gridCard = elements.problemsGrid.querySelector(`[data-problem-id="${problemId}"]`); 409 | if (gridCard) { 410 | gridCard.classList.toggle('completed', isCompleted); 411 | 412 | const button = gridCard.querySelector('.completion-btn'); 413 | const icon = button.querySelector('i'); 414 | const text = button.childNodes[button.childNodes.length - 1]; 415 | 416 | if (isCompleted) { 417 | button.classList.add('completed-button'); 418 | icon.className = 'fas fa-check'; 419 | text.textContent = ' Completed'; 420 | } else { 421 | button.classList.remove('completed-button'); 422 | icon.className = 'fas fa-plus'; 423 | text.textContent = ' Mark Done'; 424 | } 425 | } 426 | } 427 | 428 | // Filtering and sorting 429 | function applyFilters() { 430 | const searchTerm = (elements.searchInput?.value || '').toLowerCase(); 431 | const selectedDifficulty = (elements.difficultyFilter?.value || '').toLowerCase(); 432 | const selectedCompletion = (elements.completionFilter?.value || '').toLowerCase(); 433 | 434 | filteredProblems = allProblems.filter(problem => { 435 | const matchesSearch = !searchTerm || problem.title.toLowerCase().includes(searchTerm); 436 | const matchesDifficulty = !selectedDifficulty || problem.difficulty.toLowerCase() === selectedDifficulty; 437 | 438 | const isCompleted = completedProblems.has(problem.id); 439 | const matchesCompletion = !selectedCompletion || 440 | (selectedCompletion === 'completed' && isCompleted) || 441 | (selectedCompletion === 'incomplete' && !isCompleted); 442 | 443 | const matchesCompany = activeCompanies.size === 0 || 444 | problem.companies.some(c => activeCompanies.has(c)); 445 | 446 | const matchesTopic = activeTopics.size === 0 || 447 | problem.topics.some(t => activeTopics.has(t)); 448 | 449 | return matchesSearch && matchesDifficulty && matchesCompletion && matchesCompany && matchesTopic; 450 | }); 451 | 452 | sortProblems(); 453 | renderProblems(); 454 | updateClearButtonState(); 455 | savePreferences(); 456 | } 457 | 458 | function sortProblems() { 459 | filteredProblems.sort((a, b) => { 460 | let valA, valB; 461 | 462 | if (currentSort === 'numberOfCompanies') { 463 | valA = a.numberOfCompanies; 464 | valB = b.numberOfCompanies; 465 | } else if (currentSort === 'difficulty') { 466 | const diffOrder = { 'EASY': 1, 'MEDIUM': 2, 'HARD': 3 }; 467 | valA = diffOrder[a.difficulty.toUpperCase()] || 4; 468 | valB = diffOrder[b.difficulty.toUpperCase()] || 4; 469 | } else { 470 | return 0; 471 | } 472 | 473 | return sortReversed ? valA - valB : valB - valA; 474 | }); 475 | } 476 | 477 | function clearAllFilters() { 478 | if (elements.searchInput) elements.searchInput.value = ''; 479 | if (elements.difficultyFilter) elements.difficultyFilter.value = ''; 480 | if (elements.completionFilter) elements.completionFilter.value = ''; 481 | if (elements.companySearch) elements.companySearch.value = ''; 482 | if (elements.topicSearch) elements.topicSearch.value = ''; 483 | 484 | activeCompanies.clear(); 485 | activeTopics.clear(); 486 | 487 | saveFilters(); 488 | renderSelectedBubbles(); 489 | renderSearchBubbles('', 'company'); 490 | renderSearchBubbles('', 'topic'); 491 | updateClearButtonState(); 492 | applyFilters(); 493 | } 494 | 495 | function updateClearButtonState() { 496 | const hasActiveFilters = 497 | (elements.searchInput?.value || '') || 498 | (elements.difficultyFilter?.value || '') || 499 | (elements.completionFilter?.value || '') || 500 | activeCompanies.size > 0 || 501 | activeTopics.size > 0; 502 | 503 | if (elements.clearFiltersBtn) { 504 | elements.clearFiltersBtn.disabled = !hasActiveFilters; 505 | } 506 | } 507 | 508 | // Utility functions 509 | function getDifficultyColor(difficulty) { 510 | const colors = { 'EASY': 'easy', 'MEDIUM': 'medium', 'HARD': 'hard' }; 511 | return colors[difficulty.toUpperCase()] || 'medium'; 512 | } 513 | 514 | function showEmptyState() { 515 | elements.emptyState.style.display = 'block'; 516 | elements.problemsGrid.style.display = 'none'; 517 | } 518 | 519 | function hideEmptyState() { 520 | elements.emptyState.style.display = 'none'; 521 | elements.problemsGrid.style.display = 'grid'; 522 | } 523 | 524 | function hideLoading() { 525 | elements.loadingState.style.display = 'none'; 526 | } 527 | 528 | // Export/Import functionality 529 | function exportUserData() { 530 | const exportData = { 531 | version: '1.0', 532 | timestamp: new Date().toISOString(), 533 | data: { 534 | completedProblems: [...completedProblems] 535 | } 536 | }; 537 | 538 | const dataStr = JSON.stringify(exportData, null, 2); 539 | const dataBlob = new Blob([dataStr], { type: 'application/json' }); 540 | 541 | const link = document.createElement('a'); 542 | link.href = URL.createObjectURL(dataBlob); 543 | link.download = `relevant-leetcode-data-${new Date().toISOString().split('T')[0]}.json`; 544 | document.body.appendChild(link); 545 | link.click(); 546 | document.body.removeChild(link); 547 | URL.revokeObjectURL(link.href); 548 | } 549 | 550 | 551 | function validateImportData(data) { 552 | try { 553 | if (!data || typeof data !== 'object') return false; 554 | if (!data.data || typeof data.data !== 'object') return false; 555 | 556 | const { completedProblems } = data.data; 557 | 558 | // Validate completed problems 559 | if (completedProblems && !Array.isArray(completedProblems)) return false; 560 | 561 | return true; 562 | } catch (error) { 563 | console.error('Validation error:', error); 564 | return false; 565 | } 566 | } 567 | 568 | function handleFileImport(event) { 569 | console.log('File import triggered'); 570 | const file = event.target.files[0]; 571 | if (!file) return; 572 | 573 | const reader = new FileReader(); 574 | reader.onload = function(e) { 575 | try { 576 | const data = JSON.parse(e.target.result); 577 | console.log('Import data:', data); 578 | if (!validateImportData(data)) { 579 | alert('Invalid file format. Please select a valid export file.'); 580 | return; 581 | } 582 | 583 | executeImport(data); 584 | alert('Data imported successfully!'); 585 | } catch (error) { 586 | console.error('Import error:', error); 587 | alert('Error reading file. Please make sure it\'s a valid JSON file.'); 588 | } 589 | }; 590 | 591 | reader.readAsText(file); 592 | } 593 | 594 | function executeImport(data) { 595 | 596 | if (!data?.data) return; 597 | 598 | const { completedProblems: importedCompleted = []} = data.data; 599 | importedCompleted.forEach(id => { 600 | if (!completedProblems.has(id)) { 601 | toggleComplete(id); 602 | } 603 | }); 604 | } 605 | 606 | 607 | // Event listeners 608 | document.addEventListener('DOMContentLoaded', () => { 609 | loadCompletedProblems(); 610 | loadFilters(); 611 | loadPreferences(); 612 | loadProblemsData(); 613 | 614 | // Remove dangerous button entirely 615 | const dangerBtn = document.getElementById('dangerousButton'); 616 | if (dangerBtn) dangerBtn.remove(); 617 | 618 | // Export/Import event listeners 619 | elements.exportDataBtn?.addEventListener('click', exportUserData); 620 | elements.importDataBtn?.addEventListener('click', () => elements.importFileInput?.click()); 621 | elements.importFileInput?.addEventListener('change', handleFileImport); 622 | 623 | // Clear filters 624 | elements.clearFiltersBtn?.addEventListener('click', clearAllFilters); 625 | 626 | // Search and filters 627 | const debouncedFilter = debounce(applyFilters, 200); 628 | elements.searchInput?.addEventListener('input', debouncedFilter); 629 | elements.difficultyFilter?.addEventListener('change', applyFilters); 630 | elements.completionFilter?.addEventListener('change', applyFilters); 631 | 632 | // Company and topic search 633 | const debouncedCompanySearch = debounce((e) => renderSearchBubbles(e.target.value, 'company'), 200); 634 | const debouncedTopicSearch = debounce((e) => renderSearchBubbles(e.target.value, 'topic'), 200); 635 | 636 | elements.companySearch?.addEventListener('input', debouncedCompanySearch); 637 | elements.topicSearch?.addEventListener('input', debouncedTopicSearch); 638 | 639 | // Sort buttons 640 | document.querySelectorAll('.sort-btn').forEach(btn => { 641 | btn.onclick = () => { 642 | const clickedSort = btn.dataset.sort; 643 | 644 | if (currentSort === clickedSort) { 645 | sortReversed = !sortReversed; 646 | } else { 647 | currentSort = clickedSort; 648 | sortReversed = false; 649 | } 650 | 651 | updateSortButtons(); 652 | savePreferences(); 653 | sortProblems(); 654 | renderProblems(); 655 | }; 656 | }); 657 | }); --------------------------------------------------------------------------------