├── .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 |
48 |
49 |
50 |
51 |
52 |
53 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | Difficulty
64 | Easy
65 | Medium
66 | Hard
67 |
68 |
69 |
70 |
71 | All Questions
72 | Completed
73 | Incomplete
74 |
75 |
76 |
77 |
78 |
79 | Clear All
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
97 |
98 |
101 |
102 |
105 |
106 |
107 |
108 |
118 |
119 |
122 |
123 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
152 |
153 |
154 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
No problems found
167 |
Try adjusting your filters or search terms
168 |
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 |
342 |
343 |
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 |
369 |
370 | Solve
371 |
372 |
373 |
374 | ${isCompleted ? 'Completed' : 'Mark Done'}
375 |
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 | });
--------------------------------------------------------------------------------