-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpopup.js
More file actions
1768 lines (1535 loc) · 68.6 KB
/
popup.js
File metadata and controls
1768 lines (1535 loc) · 68.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Valid options for combobox inputs
const VALID_TOPICS = ['Array','Backtracking','Biconnected Component','Binary Indexed Tree','Binary Search','Binary Search Tree','Binary Tree','Bit Manipulation','Bitmask','Brainteaser','Breadth-First Search','Bucket Sort','Combinatorics','Concurrency','Counting','Counting Sort','Data Stream','Database','Depth-First Search','Design','Divide and Conquer','Doubly-Linked List','Dynamic Programming','Enumeration','Eulerian Circuit','Game Theory','Geometry','Graph','Greedy','Hash Function','Hash Table','Heap (Priority Queue)','Interactive','Iterator','Line Sweep','Linked List','Math','Matrix','Memoization','Merge Sort','Minimum Spanning Tree','Monotonic Queue','Monotonic Stack','Number Theory','Ordered Set','Prefix Sum','Probability and Statistics','Queue','Quickselect','Radix Sort','Randomized','Recursion','Rejection Sampling','Reservoir Sampling','Rolling Hash','Segment Tree','Shell','Shortest Path','Simulation','Sliding Window','Sort','Sorting','Stack','String','String Matching','Strongly Connected Component','Suffix Array','Topological Sort','Tree','Trie','Two Pointers','Union Find'];
const VALID_COMPANIES = ['1Kosmos','23&me','6sense','AMD','APT Portfolio','AQR Capital Management','ASUS','AT&T','Accelya','Accenture','Accolite','Acko','Activevideo','Activision','Addepar','Adobe','Aetion','Affinity','Affirm','Agoda','Airbnb','Airbus SE','Airtel','Ajira','Akamai','Akuna','Akuna Capital','Alibaba','AllinCall','Alphonso','Altimetrik','Amazon','Amadeus','American Express','Amplitude','Anaplan','Ancestry','Anduril','Angi','Ant Group','Apple','Applied Intuition','Arcesium','Arclight','Ares Management','Arista Networks','Asana','Ascend Learning','Atlassian','Aurora','Autodesk','Avalara','Avito','Axon','Baidu','Barclays','Benchling','BitGo','Bloomberg','Bolt','Box','Brex','Bristol-Myers Squibb','Broadcom','C3.AI','Cadence','Capital One','Careem','Carta','Cashfree','Chime','Citadel','Citrix','Cisco','Cloudera','Cloudflare','Clover Health','Codenation','Coinbase','Comcast','Coupang','Cruise','Databricks','Datadog','Dataminr','De Shaw','Dell','Deutsche Bank','DoorDash','Dropbox','DRW','Duolingo','eBay','Electronic Arts','Expedia','Expensify','Facebook','Fidelity','Figma','FlipKart','Flipkart','Foursquare','GE Healthcare','GoDaddy','Goldman Sachs','Google','Grab','Groupon','HRT','HackerRank','HashiCorp','Hims & Hers','Houzz','Huawei','IBM','IIT','IXL','Indeed','Infosys','Instagram','Instacart','Intel','Intuit','Jio','JP Morgan','Jane Street','Johnson & Johnson','Jpmorgan','Kakao','Karat','Kargo','Klarna','Kustomer','LinkedIn','Loft','Looker','Lyft','MakeMyTrip','Mathworks','Media.net','Meta','Microsoft','Mindtickle','Miro','Morgan Stanley','Myntra','Nagarro','NCR','Netflix','Niantic','Nvidia','Nykaa','OKX','Oracle','Oyo','Paytm','Paypal','Palo Alto Networks','Palantir Technologies','Pinterest','Pocket Gems','Pony.ai','Postmates','PwC','Qualcomm','Qualtrics','Quora','Razorpay','Redfin','Roblox','Robinhood','Salesforce','Samsung','SAP','Seagate','ServiceNow','Shopee','Shopify','Siemens','Slack','Snapchat','Snowflake','SpaceX','Splunk','Spotify','Square','Stripe','Swiggy','Synopsys','TCS','TikTok','Twitter','Two Sigma','Twitch','Twilio','Uber','Unity','Veritas','VMware','Visa','Walmart','Wayfair','Waymo','Wells Fargo','Wish','Wolfe Research','Workday','Yahoo','Yandex','Yelp','Zenefits','Zendesk','Zillow','Zomato','Zoom','Zscaler','eBay','FactSet','Groupon','Hulu','Kaspersky','NerdWallet','Okta','Plaid','Ramp','Scale AI','Sentry','Squarespace','Stitch Fix','Taboola','TaskUs','ThoughtWorks','Twitch','Yatra','Zuora'];
// Helper for LeetCode GraphQL requests
async function leetcodeFetch(body) {
return fetch("https://leetcode.com/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(body)
});
}
// List helpers - inlined for Chrome extension compatibility
const listDataCache = {};
async function loadListData(listName) {
if (listDataCache[listName]) {
return listDataCache[listName];
}
try {
const response = await fetch(chrome.runtime.getURL(`data/${listName}.json`));
const data = await response.json();
listDataCache[listName] = data;
return data;
} catch (error) {
console.error(`Error loading ${listName} data:`, error);
return null;
}
}
async function loadMasterProblems() {
if (listDataCache['master']) {
return listDataCache['master'];
}
try {
const response = await fetch(chrome.runtime.getURL('data/leetcode-problems.json'));
const data = await response.json();
const problemMap = {};
for (const problem of data.problems) {
problemMap[problem.id] = problem;
}
const masterData = { ...data, problemMap };
listDataCache['master'] = masterData;
return masterData;
} catch (error) {
console.error('Error loading master problems:', error);
return null;
}
}
async function getListStats(listName, completedIds = []) {
const listData = await loadListData(listName);
const master = await loadMasterProblems();
if (!listData || !master || !listData.categories) {
return { total: 0, completed: 0, remaining: 0, percentage: 0 };
}
const completedSet = new Set(completedIds.map(String));
let total = 0;
let completed = 0;
for (const category of listData.categories) {
if (!category.problemIds) continue;
for (const problemId of category.problemIds) {
total++;
if (completedSet.has(String(problemId))) {
completed++;
}
}
}
const remaining = total - completed;
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
return { total, completed, remaining, percentage };
}
// Pick a random element from an array
function pickRandom(arr) {
return arr.length > 0 ? arr[Math.floor(Math.random() * arr.length)] : null;
}
async function getNextUnsolvedForList(listName) {
const [listData, master] = await Promise.all([loadListData(listName), loadMasterProblems()]);
if (!listData || !master) return null;
return new Promise(resolve => {
chrome.storage.local.get(['completedProblemIds'], (result) => {
const done = new Set((result.completedProblemIds || []).map(String));
const seen = new Set();
const candidates = [];
for (const cat of listData.categories || []) {
for (const id of cat.problemIds || []) {
if (seen.has(id)) continue;
seen.add(id);
if (!done.has(String(id))) {
const p = master.problemMap[id];
if (p && !p.isPaidOnly) candidates.push(p.url);
}
}
}
resolve(pickRandom(candidates));
});
});
}
async function getNextUnsolvedForTag(name, type) {
const master = await loadMasterProblems();
if (!master) return null;
return new Promise(resolve => {
chrome.storage.local.get(['completedProblemIds'], (result) => {
const done = new Set((result.completedProblemIds || []).map(String));
const candidates = [];
for (const p of master.problems) {
if (done.has(String(p.id)) || p.isPaidOnly) continue;
if (type === 'topic') {
const topics = (p.topics || []).map(t => typeof t === 'string' ? t : t.name);
if (topics.some(t => t.toLowerCase() === name.toLowerCase())) candidates.push(p.url);
} else {
if ((p.companies || []).some(c => c.toLowerCase() === name.toLowerCase())) candidates.push(p.url);
}
}
resolve(pickRandom(candidates));
});
});
}
async function getNextUnsolvedForIntersection(topics, companies) {
const master = await loadMasterProblems();
if (!master) return null;
return new Promise(resolve => {
chrome.storage.local.get(['completedProblemIds'], (result) => {
const done = new Set((result.completedProblemIds || []).map(String));
const candidates = [];
for (const p of master.problems) {
if (done.has(String(p.id)) || p.isPaidOnly) continue;
const pt = (p.topics || []).map(t => typeof t === 'string' ? t : t.name);
const pc = p.companies || [];
const matchT = topics.some(t => pt.some(x => x.toLowerCase() === t.toLowerCase()));
const matchC = companies.some(c => pc.some(x => x.toLowerCase() === c.toLowerCase()));
if (matchT && matchC) candidates.push(p.url);
}
resolve(pickRandom(candidates));
});
});
}
function getTodayDate() {
return new Date().toISOString().slice(0, 10);
}
// Cache for problem data from JSON file
let problemDataCache = null;
async function loadProblemData() {
if (problemDataCache) return problemDataCache;
try {
const url = chrome.runtime.getURL('data/leetcode-problems.json');
const response = await fetch(url);
const data = await response.json();
// Create a map for quick lookup by titleSlug
problemDataCache = new Map();
data.problems.forEach(p => {
problemDataCache.set(p.titleSlug, p);
});
return problemDataCache;
} catch (error) {
console.error("Failed to load problem data:", error);
return null;
}
}
async function getProblemCompanyData(titleSlug) {
const cache = await loadProblemData();
if (!cache) return null;
const problem = cache.get(titleSlug);
if (!problem) return null;
return {
companies: problem.companies || [],
companyFrequency: problem.companyFrequency || {},
likes: problem.likes || 0,
dislikes: problem.dislikes || 0,
hints: problem.hints || [],
similarQuestions: problem.similarQuestions || []
};
}
async function fetchLeetCodeUserData() {
try {
const response = await leetcodeFetch({
query: `
query globalData {
userStatus {
username
isSignedIn
avatar
}
streakCounter {
streakCount
}
activeDailyCodingChallengeQuestion {
userStatus
}
}
`
});
const data = await response.json();
const userStatus = data?.data?.userStatus;
if (!userStatus?.isSignedIn) {
return null;
}
const streakData = data?.data?.streakCounter;
const dailyStatus = data?.data?.activeDailyCodingChallengeQuestion?.userStatus;
// Check if today's daily challenge is completed
// Only use dailyStatus === "Finish" as the definitive check
const completedToday = dailyStatus === "Finish";
return {
username: userStatus.username,
avatar: userStatus.avatar,
streak: streakData?.streakCount || 0,
completedToday
};
} catch (error) {
console.error("Failed to fetch LeetCode user data:", error);
return null;
}
}
function updateTimerDisplay() {
const now = new Date();
const nextUTC = new Date(now);
nextUTC.setUTCHours(0, 0, 0, 0);
nextUTC.setUTCDate(nextUTC.getUTCDate() + 1);
const diff = nextUTC - now;
const hours = Math.floor(diff / 3600000);
const minutes = Math.floor((diff % 3600000) / 60000);
document.getElementById("timer").textContent = `New in: ${hours}h ${minutes}m`;
}
async function getDailyQuestionSlug() {
const query = {
query: `
query questionOfToday {
activeDailyCodingChallengeQuestion {
date
question {
titleSlug
title
difficulty
questionFrontendId
stats
topicTags { name }
}
}
}
`
};
const response = await leetcodeFetch(query);
const data = await response.json();
const challenge = data.data.activeDailyCodingChallengeQuestion;
return { ...challenge.question, date: challenge.date };
}
// Fetch all solved problem IDs from LeetCode and merge into chrome.storage
async function syncCompletedProblemIds() {
try {
// Get existing IDs first
const existing = await new Promise(resolve => {
chrome.storage.local.get(['completedProblemIds'], r => resolve(r.completedProblemIds || []));
});
const response = await leetcodeFetch({
query: `query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) {
problemsetQuestionList: questionList(categorySlug: $categorySlug, limit: $limit, skip: $skip, filters: $filters) {
totalNum
data { questionFrontendId status }
}
}`,
variables: { categorySlug: "", limit: 3000, skip: 0, filters: {} }
});
const data = await response.json();
const questions = data?.data?.problemsetQuestionList?.data || [];
const apiIds = questions
.filter(q => q.status === "ac")
.map(q => parseInt(q.questionFrontendId))
.filter(id => !isNaN(id));
// Synced completed IDs from LeetCode
if (apiIds.length > 0) {
// Merge: union of existing + API results
const merged = [...new Set([...existing.map(Number), ...apiIds])];
await new Promise(resolve => {
chrome.storage.local.set({ completedProblemIds: merged }, resolve);
});
return merged;
}
return existing;
} catch (err) {
console.error("Failed to sync completed problem IDs:", err);
return [];
}
}
async function fetchUserSolvedStats(username) {
try {
const response = await leetcodeFetch({
query: `
query userProblemsSolved($username: String!) {
matchedUser(username: $username) {
submitStatsGlobal {
acSubmissionNum {
difficulty
count
}
}
}
}
`,
variables: { username }
});
const data = await response.json();
const stats = data?.data?.matchedUser?.submitStatsGlobal?.acSubmissionNum || [];
return {
easy: stats.find(s => s.difficulty === "Easy")?.count || 0,
medium: stats.find(s => s.difficulty === "Medium")?.count || 0,
hard: stats.find(s => s.difficulty === "Hard")?.count || 0,
total: stats.find(s => s.difficulty === "All")?.count || 0
};
} catch (error) {
console.error("Failed to fetch solved stats:", error);
return null;
}
}
async function fetchLast30DaysHistory(username) {
try {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
// Fetch daily challenge status for current and last month
const fetchDailyChallenges = async (y, m) => {
const response = await leetcodeFetch({
query: `
query dailyCodingQuestionRecords($year: Int!, $month: Int!) {
dailyCodingChallengeV2(year: $year, month: $month) {
challenges {
date
userStatus
}
}
}
`,
variables: { year: y, month: m }
});
const data = await response.json();
return data?.data?.dailyCodingChallengeV2?.challenges || [];
};
// Fetch submission calendar (problems solved per day)
const fetchSubmissionCalendar = async () => {
if (!username) return {};
const response = await leetcodeFetch({
query: `
query userProfileCalendar($username: String!) {
matchedUser(username: $username) {
userCalendar {
submissionCalendar
}
}
}
`,
variables: { username }
});
const data = await response.json();
const calendarStr = data?.data?.matchedUser?.userCalendar?.submissionCalendar;
return calendarStr ? JSON.parse(calendarStr) : {};
};
// Fetch all data in parallel
const [currentMonth, lastMonthData, submissionCalendar] = await Promise.all([
fetchDailyChallenges(year, month),
fetchDailyChallenges(month === 1 ? year - 1 : year, month === 1 ? 12 : month - 1),
fetchSubmissionCalendar()
]);
const allChallenges = [...currentMonth, ...lastMonthData];
// Build daily challenge map
const dailyChallengeMap = new Map();
allChallenges.forEach(c => {
dailyChallengeMap.set(c.date, c.userStatus === "Finish");
});
// Convert submission calendar (unix timestamps) to date -> count map
const submissionMap = new Map();
for (const [timestamp, count] of Object.entries(submissionCalendar)) {
const date = new Date(parseInt(timestamp) * 1000);
const dateStr = date.toISOString().slice(0, 10);
submissionMap.set(dateStr, count);
}
return { dailyChallengeMap, submissionMap };
} catch (error) {
console.error("Failed to fetch 30-day history:", error);
return { dailyChallengeMap: new Map(), submissionMap: new Map() };
}
}
// Renders chips inline with "+X more" that expands on click
function renderChipsWithOverflow(container, items, chipClass, moreClass, isTopics) {
container.innerHTML = '';
const containerWidth = container.offsetWidth;
let usedWidth = 0;
let visibleCount = 0;
const gap = 6; // gap-1.5 = 6px
const moreButtonWidth = 60; // approximate width for "+X more"
// Create all chips first to measure
const chips = items.map((item) => {
const chip = document.createElement('span');
chip.className = chipClass;
if (isTopics) {
// Topics: item is { name, slug } or has .name
const tagName = item.name || item;
const tagSlug = encodeURIComponent(
tagName.toLowerCase()
.replace(/[ ()]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
);
chip.textContent = tagName;
chip.dataset.tag = tagSlug;
chip.style.cursor = 'pointer';
} else {
// Companies: item is { name, freq }
const freqLabel = item.freq > 0 ? ` (${item.freq})` : '';
chip.textContent = `${item.name}${freqLabel}`;
chip.dataset.company = item.name;
chip.style.cursor = 'pointer';
}
return chip;
});
// Measure and add visible chips
const tempContainer = document.createElement('div');
tempContainer.style.cssText = 'position:absolute;visibility:hidden;display:flex;gap:6px;';
document.body.appendChild(tempContainer);
for (let i = 0; i < chips.length; i++) {
tempContainer.appendChild(chips[i].cloneNode(true));
const chipWidth = tempContainer.lastChild.offsetWidth;
const remainingItems = chips.length - (i + 1);
const needsMore = remainingItems > 0;
const requiredWidth = usedWidth + chipWidth + (needsMore ? moreButtonWidth + gap : 0);
if (requiredWidth <= containerWidth || i === 0) {
usedWidth += chipWidth + gap;
visibleCount++;
} else {
break;
}
}
document.body.removeChild(tempContainer);
// Add visible chips
const visibleChips = chips.slice(0, visibleCount);
visibleChips.forEach(chip => container.appendChild(chip));
// Add "+X more" button if needed
const hiddenCount = chips.length - visibleCount;
if (hiddenCount > 0) {
const moreBtn = document.createElement('span');
moreBtn.className = moreClass;
moreBtn.textContent = `+${hiddenCount} more`;
moreBtn.style.cursor = 'pointer';
moreBtn.dataset.expanded = 'false';
moreBtn.addEventListener('click', () => {
const isExpanded = moreBtn.dataset.expanded === 'true';
if (isExpanded) {
// Collapse: remove hidden chips and reset button
container.querySelectorAll('[data-hidden="true"]').forEach(el => el.remove());
container.classList.remove('flex-wrap');
container.classList.add('overflow-hidden');
moreBtn.textContent = `+${hiddenCount} more`;
moreBtn.dataset.expanded = 'false';
} else {
// Expand: add hidden chips before the button and allow wrapping
container.classList.add('flex-wrap');
container.classList.remove('overflow-hidden');
const hiddenChips = chips.slice(visibleCount);
hiddenChips.forEach(chip => {
chip.dataset.hidden = 'true';
container.insertBefore(chip, moreBtn);
});
moreBtn.textContent = 'less';
moreBtn.dataset.expanded = 'true';
}
});
container.appendChild(moreBtn);
}
// Add click handlers for chips → open explorer with filter
container.addEventListener('click', (event) => {
const target = event.target;
if (isTopics && target.dataset.tag) {
const topicName = target.textContent.trim();
chrome.tabs.create({ url: getExplorerUrl([topicName], []) });
} else if (!isTopics && target.dataset.company) {
chrome.tabs.create({ url: getExplorerUrl([], [target.dataset.company]) });
}
});
}
function renderQuestion(question, companyData = null) {
// LeetCode's exact difficulty colors
const difficultyColors = {
Easy: "text-[#00b8a3]",
Medium: "text-[#ffa116]",
Hard: "text-[#ff375f]",
};
const difficultyColor = difficultyColors[question.difficulty] || "text-[#eff1f699]";
let acceptanceRate = "N/A";
try {
const stats = JSON.parse(question.stats || "{}");
acceptanceRate = stats.acRate ? parseFloat(stats.acRate).toFixed(2) : "N/A";
} catch {
acceptanceRate = "N/A";
}
// Consistent chip styling - same size/padding, different colors
const baseChipClass = "inline-flex items-center px-2 py-0.5 rounded text-[11px] font-medium transition-colors whitespace-nowrap";
const topicChipClass = `${baseChipClass} bg-[#ffffff0d] text-[#eff1f699] hover:bg-[#ffffff1a] hover:text-[#eff1f6] cursor-pointer`;
const companyChipClass = `${baseChipClass} bg-[#ffa1161a] text-[#ffa116] hover:bg-[#ffa11633] cursor-pointer`;
const moreChipClass = `${baseChipClass} bg-[#ffffff0d] text-[#eff1f699] hover:bg-[#ffffff1a] hover:text-[#eff1f6] cursor-pointer`;
const topicsArray = question.topicTags || [];
const hasCompanyData = companyData && companyData.companies && companyData.companies.length > 0;
const companiesArray = hasCompanyData ? companyData.companies : [];
const problemUrl = `https://leetcode.com/problems/${question.titleSlug}`;
// Book icon for topics
const bookIcon = `<svg class="w-3 h-3 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>`;
// Building icon for companies
const buildingIcon = `<svg class="w-3 h-3 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18"></path><path d="M9 8h1"></path><path d="M9 12h1"></path><path d="M9 16h1"></path><path d="M14 8h1"></path><path d="M14 12h1"></path><path d="M14 16h1"></path><path d="M5 21V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16"></path></svg>`;
document.getElementById("question").innerHTML = `
<div class="mb-3 flex items-start gap-2">
<div class="flex-1 text-[14px] leading-snug"><span class="text-[#eff1f699]">${question.questionFrontendId}.</span> <span class="font-medium text-[#eff1f6]">${question.title}</span> <span style="white-space: nowrap; font-size: 11px; float: right;"><span class="font-medium ${difficultyColor}">${question.difficulty}</span><span style="color: #eff1f699;"> · </span><span id="acceptance-rate" style="color: #eff1f699; cursor: help;">${acceptanceRate}%</span></span></div>
<button id="open-problem" class="text-[#eff1f6] hover:text-[#ffa116] cursor-pointer transition-colors flex-shrink-0 mt-0.5" title="Open problem">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
</button>
</div>
<div>
<!-- Topics row -->
<div class="flex items-start gap-2 text-[#eff1f699] px-2.5 py-2 border border-[#ffffff1a]" style="border-radius: 8px 8px 0 0; border-bottom: none;">
<div class="flex items-center gap-1.5 flex-shrink-0">
${bookIcon}
<span class="text-[11px] font-medium">Topics</span>
<span class="text-[#ffffff33]">›</span>
</div>
<div id="topics-row" class="flex-1 flex items-center gap-1.5 overflow-hidden min-w-0">
<!-- Chips injected by JS -->
</div>
</div>
<!-- Companies row -->
<div class="flex items-start gap-2 text-[#eff1f699] px-2.5 py-2 border border-[#ffffff1a] border-t-0" style="border-radius: 0 0 8px 8px;">
<div class="flex items-center gap-1.5 flex-shrink-0">
${buildingIcon}
<span class="text-[11px] font-medium">Companies</span>
<span class="text-[#ffffff33]">›</span>
</div>
<div id="companies-row" class="flex-1 flex items-center gap-1.5 overflow-hidden min-w-0">
${hasCompanyData ? '<!-- Chips injected by JS -->' : '<span class="text-[11px] text-[#eff1f666]">Premium data shows no company tags</span>'}
</div>
</div>
</div>
`;
// Render topic chips with "+X more" logic
const topicsRowEl = document.getElementById("topics-row");
if (topicsRowEl && topicsArray.length > 0) {
renderChipsWithOverflow(topicsRowEl, topicsArray, topicChipClass, moreChipClass, true);
} else if (topicsRowEl) {
topicsRowEl.innerHTML = '<span class="text-[11px] text-[#eff1f666]">N/A</span>';
}
// Render company chips with "+X more" logic (only if there's data)
const companiesRowEl = document.getElementById("companies-row");
if (companiesRowEl && hasCompanyData && companiesArray.length > 0) {
const companyItems = companiesArray.map(company => {
const freq = companyData.companyFrequency[company] || 0;
return { name: company, freq };
});
renderChipsWithOverflow(companiesRowEl, companyItems, companyChipClass, moreChipClass, false);
}
// Open problem button
document.getElementById("open-problem").addEventListener("click", () => {
chrome.tabs.create({ url: problemUrl });
});
// Acceptance rate tooltip
const acceptanceEl = document.getElementById("acceptance-rate");
if (acceptanceEl) {
let tooltip = null;
acceptanceEl.addEventListener("mouseenter", () => {
tooltip = document.createElement("div");
tooltip.textContent = `${acceptanceRate}% Acceptance Rate`;
tooltip.style.cssText = "position:absolute;background:#333;color:#fff;padding:4px 8px;border-radius:4px;font-size:10px;white-space:nowrap;z-index:1000;";
const rect = acceptanceEl.getBoundingClientRect();
tooltip.style.left = rect.left + "px";
tooltip.style.top = (rect.top - 28) + "px";
document.body.appendChild(tooltip);
});
acceptanceEl.addEventListener("mouseleave", () => {
if (tooltip) {
tooltip.remove();
tooltip = null;
}
});
}
}
document.addEventListener("DOMContentLoaded", async () => {
let question;
try {
question = await getDailyQuestionSlug();
const companyData = await getProblemCompanyData(question.titleSlug);
renderQuestion(question, companyData);
} catch (err) {
console.error('Failed to load daily challenge:', err);
const questionEl = document.getElementById("question");
if (questionEl) {
questionEl.innerHTML = `<div class="text-[12px] text-[#eff1f699]">Could not load daily challenge. <a href="https://leetcode.com/problemset/" target="_blank" class="text-[#ffa116] hover:underline">Open LeetCode</a></div>`;
}
}
updateTimerDisplay();
setInterval(updateTimerDisplay, 60 * 1000);
function getStreakMilestone(streak) {
const milestones = [
{ days: 365, emoji: "👑", message: "Legendary! 1 year streak!" },
{ days: 100, emoji: "💯", message: "Amazing! 100 day streak!" },
{ days: 50, emoji: "⭐", message: "Fantastic! 50 day streak!" },
{ days: 30, emoji: "🏆", message: "Incredible! 30 day streak!" },
{ days: 14, emoji: "🎯", message: "Two weeks strong!" },
{ days: 7, emoji: "🌟", message: "One week streak!" },
];
return milestones.find(m => streak >= m.days) || null;
}
function updateStreakDisplay() {
chrome.storage.local.get([
"currentStreak",
"longestStreak",
"lastSolvedDate",
"leetCodeUsername"
], (result) => {
const streak = result.currentStreak || 0;
const longestStreak = result.longestStreak || 0;
const today = getTodayDate();
const lastSolved = result.lastSolvedDate || null;
const streakDisplay = document.getElementById("streakDisplay");
const username = result.leetCodeUsername;
const milestone = getStreakMilestone(streak);
const solvedToday = lastSolved === today;
if (solvedToday) {
// Solved today - show active streak
const milestoneEmoji = milestone ? ` ${milestone.emoji}` : "";
const prev = streakDisplay.textContent;
streakDisplay.textContent = `🔥 ${streak}${milestoneEmoji}`;
streakDisplay.title = milestone
? `${milestone.message} ${username ? `(${username})` : ""}`
: `Streak active! ${streak} day${streak !== 1 ? 's' : ''}`;
// Pulse animation when streak value changes
if (prev && prev !== streakDisplay.textContent) {
streakDisplay.classList.add('streak-pulse');
setTimeout(() => streakDisplay.classList.remove('streak-pulse'), 300);
}
} else {
// Not solved today - show pending streak
streakDisplay.textContent = `🔥 ${streak}`;
streakDisplay.title = streak > 0
? `Streak at risk! Keep solving to continue your ${streak}-day streak.`
: `Start your streak by solving a problem!`;
}
// Show milestone celebration banner if applicable
const milestoneEl = document.getElementById("milestone-banner");
if (milestoneEl && milestone && solvedToday) {
milestoneEl.textContent = `${milestone.emoji} ${milestone.message}`;
milestoneEl.classList.remove("hidden");
} else if (milestoneEl) {
milestoneEl.classList.add("hidden");
}
});
}
// Show/hide login prompt and stats panel based on login state
function updateLoginState(isLoggedIn, userData = null) {
const loginPrompt = document.getElementById("login-prompt");
const statsPanel = document.getElementById("stats-panel");
const avatar = document.getElementById("user-avatar");
const logo = document.getElementById("lc-logo");
const headerTitle = document.getElementById("header-title");
const headerSubtitle = document.getElementById("header-subtitle");
if (isLoggedIn && userData) {
loginPrompt.classList.add("hidden");
statsPanel.classList.remove("hidden");
// Show avatar, hide logo
if (userData.avatar) {
avatar.src = userData.avatar;
avatar.alt = userData.username;
avatar.classList.remove("hidden");
logo.classList.add("hidden");
}
// Update header text
headerTitle.textContent = userData.username;
headerSubtitle.textContent = "Daily LeetCode Kro";
} else {
loginPrompt.classList.remove("hidden");
statsPanel.classList.add("hidden");
// Show logo, hide avatar
avatar.classList.add("hidden");
logo.classList.remove("hidden");
// Reset header text
headerTitle.textContent = "LeetDaily";
headerSubtitle.textContent = "Daily LeetCode Kro";
}
}
// Sync streak from LeetCode on popup open (fetch from popup since it has cookie access)
async function syncFromLeetCode() {
const userData = await fetchLeetCodeUserData();
if (userData) {
const today = getTodayDate();
updateLoginState(true, userData);
// Sync all solved problem IDs from LeetCode, then re-render list progress
syncCompletedProblemIds().then(() => {
renderListProgress();
// Tag progress already rendered from storage load; list progress
// updates in-place (no flash), but tag progress rebuilds DOM so skip it
});
chrome.storage.local.set({
streak: userData.streak,
leetCodeUsername: userData.username,
leetCodeAvatar: userData.avatar,
lastVisitedDate: userData.completedToday ? today : null,
lastSyncedAt: Date.now()
}, () => {
updateStreakDisplay();
renderStatsPanel(userData.username);
render30DayHeatmap(userData.username);
// Notify background to update badge
chrome.runtime.sendMessage({ action: "updateBadge" });
});
} else {
// Not logged in
updateLoginState(false);
render30DayHeatmap(null);
}
}
// Render stats panel
async function renderStatsPanel(username) {
if (!username) return;
const stats = await fetchUserSolvedStats(username);
if (stats) {
document.getElementById("easy-count").textContent = stats.easy;
document.getElementById("medium-count").textContent = stats.medium;
document.getElementById("hard-count").textContent = stats.hard;
document.getElementById("total-count").textContent = stats.total;
// Swap skeleton for real content
const skeleton = document.getElementById("stats-skeleton");
const content = document.getElementById("stats-content");
if (skeleton) skeleton.classList.add("hidden");
if (content) content.classList.remove("hidden");
}
}
// Render 30-day heatmap with color intensity based on problems solved
async function render30DayHeatmap(username) {
const heatmapEl = document.getElementById("heatmap");
const countEl = document.getElementById("heatmap-count");
const datesEl = document.getElementById("heatmap-dates");
// Guard against null elements
if (!heatmapEl || !countEl || !datesEl) return;
const { dailyChallengeMap, submissionMap } = await fetchLast30DaysHistory(username);
const today = new Date();
let dailyChallengeCount = 0;
// Get color class based on submission count
function getIntensityClass(count) {
if (count === 0) return "bg-[#ffffff0d]";
if (count <= 2) return "bg-[#2cbb5d40]";
if (count <= 5) return "bg-[#2cbb5d80]";
return "bg-[#2cbb5d]";
}
// Build grid: 30 cells for last 30 days
const days = [];
for (let i = 29; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().slice(0, 10);
const dailyCompleted = dailyChallengeMap.get(dateStr) === true;
const submissionCount = submissionMap.get(dateStr) || 0;
const isToday = i === 0;
if (dailyCompleted) dailyChallengeCount++;
// Color intensity based on submission count
let cellClass = getIntensityClass(submissionCount);
// Add today's ring indicator
if (isToday && submissionCount === 0) {
cellClass = "bg-[#ffa11640] ring-1 ring-[#ffa116]";
} else if (isToday) {
cellClass += " ring-1 ring-[#ffa116]";
}
const dayName = date.toLocaleDateString('en-US', { weekday: 'short' });
const submissionsText = submissionCount === 1 ? "1 submission" : `${submissionCount} submissions`;
const tooltip = `${dayName}, ${date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}: ${submissionsText}`;
// Show checkmark for daily challenge completion
const checkmark = dailyCompleted
? `<svg class="w-2 h-2 text-white" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="2 6 5 9 10 3"></polyline></svg>`
: '';
days.push(`
<div class="heatmap-cell w-full aspect-square rounded-sm ${cellClass} transition-all hover:scale-110 cursor-default flex items-center justify-center" data-tooltip="${tooltip}">${checkmark}</div>
`);
}
heatmapEl.innerHTML = days.join('');
// Setup custom tooltip for heatmap cells
const tooltipEl = document.getElementById("custom-tooltip");
const popupWidth = 400; // Extension popup width
heatmapEl.querySelectorAll(".heatmap-cell").forEach(cell => {
cell.addEventListener("mouseenter", () => {
const text = cell.dataset.tooltip;
if (text) {
tooltipEl.textContent = text;
tooltipEl.classList.remove("opacity-0");
tooltipEl.classList.add("opacity-100");
const rect = cell.getBoundingClientRect();
const tooltipWidth = tooltipEl.offsetWidth;
// Calculate centered position
let left = rect.left + rect.width / 2 - tooltipWidth / 2;
// Clamp to prevent overflow on left/right edges
const padding = 8;
left = Math.max(padding, Math.min(left, popupWidth - tooltipWidth - padding));
tooltipEl.style.left = `${left}px`;
tooltipEl.style.top = `${rect.top - tooltipEl.offsetHeight - 6}px`;
}
});
cell.addEventListener("mouseleave", () => {
tooltipEl.classList.remove("opacity-100");
tooltipEl.classList.add("opacity-0");
});
});
// Update count display
countEl.textContent = `${dailyChallengeCount}/30 daily`;
// Add date labels
const startDate = new Date(today);
startDate.setDate(startDate.getDate() - 29);
datesEl.innerHTML = `
<span>${startDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}</span>
<span>Today</span>
`;
}
// Render list progress cards
async function renderListProgress() {
try {
// Get list of completed problem IDs from user's LeetCode history
const { completedProblemIds = [] } = await new Promise(resolve => {
chrome.storage.local.get(['completedProblemIds'], resolve);
});
// Get stats for each list
const [blind75Stats, neetcode150Stats, leetcode75Stats] = await Promise.all([
getListStats('blind75', completedProblemIds),
getListStats('neetcode150', completedProblemIds),
getListStats('leetcode75', completedProblemIds)
]);
updateProgressCard('blind75', blind75Stats.completed, blind75Stats.total, blind75Stats.percentage);
updateProgressCard('neetcode150', neetcode150Stats.completed, neetcode150Stats.total, neetcode150Stats.percentage);
updateProgressCard('leetcode75', leetcode75Stats.completed, leetcode75Stats.total, leetcode75Stats.percentage);
} catch (error) {
console.error('❌ Failed to render list progress:', error);
console.error(error.stack);
// Show 0% progress on error
updateProgressCard('blind75', 0, 74, 0);
updateProgressCard('neetcode150', 0, 158, 0);
updateProgressCard('leetcode75', 0, 75, 0);
}
}
// Update a single progress card (horizontal bar)
function updateProgressCard(listName, completed, total, percentage) {
const progressBar = document.getElementById(`${listName}-progress`);
const percentageText = document.getElementById(`${listName}-percentage`);
const countText = document.getElementById(`${listName}-count`);
if (progressBar) {
progressBar.style.width = `${percentage}%`;
progressBar.classList.add('progress-animate');
}
if (percentageText) percentageText.textContent = `${percentage}%`;
if (countText) countText.textContent = `${completed}/${total}`;
}
// Add click handlers for list labels
[['blind75-label', 'blind75'], ['neetcode150-label', 'neetcode150'], ['leetcode75-label', 'leetcode75']].forEach(([labelId, listName]) => {
const label = document.getElementById(labelId);
if (label) {
label.addEventListener('click', () => {
chrome.tabs.create({ url: chrome.runtime.getURL(`problems-explorer.html?list=${listName}`) });
});
}
});
// Next unsolved buttons for list cards
[['blind75-next', 'blind75'], ['neetcode150-next', 'neetcode150'], ['leetcode75-next', 'leetcode75']].forEach(([btnId, listName]) => {
const btn = document.getElementById(btnId);
if (btn) {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const url = await getNextUnsolvedForList(listName);
if (url) chrome.tabs.create({ url });
});