Skip to content

Commit b201526

Browse files
committed
list LeetCode questions
1 parent cbe49a7 commit b201526

File tree

11 files changed

+235
-36
lines changed

11 files changed

+235
-36
lines changed

jupyterlab_leetcode/handlers/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from .base_handler import BaseHandler
44
from .cookie_handler import GetCookieHandler
5-
from .leetcode_handler import LeetCodeProfileHandler, LeetCodeStatisticsHandler
5+
from .leetcode_handler import (LeetCodeProfileHandler, LeetCodeQuestionHandler,
6+
LeetCodeStatisticsHandler)
67

78

89
def setup_handlers(web_app):
@@ -12,6 +13,7 @@ def setup_handlers(web_app):
1213
GetCookieHandler,
1314
LeetCodeProfileHandler,
1415
LeetCodeStatisticsHandler,
16+
LeetCodeQuestionHandler,
1517
]
1618

1719
web_app.add_handlers(

jupyterlab_leetcode/handlers/leetcode_handler.py

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import json
2-
from typing import cast
2+
from typing import Any, Mapping, cast
33

44
import tornado
55
from tornado.gen import multi
@@ -10,7 +10,7 @@
1010

1111
LEETCODE_GRAPHQL_URL = "https://leetcode.com/graphql"
1212

13-
type QueryType = dict[str, str | dict[str, str]]
13+
type QueryType = dict[str, str | Mapping[str, Any]]
1414

1515

1616
class LeetCodeHandler(BaseHandler):
@@ -174,3 +174,93 @@ async def get(self):
174174
)
175175
)
176176
self.finish(res)
177+
178+
179+
class LeetCodeQuestionHandler(LeetCodeHandler):
180+
route = r"leetcode/questions"
181+
182+
@tornado.web.authenticated
183+
async def post(self):
184+
body = self.get_json_body()
185+
if not body:
186+
self.set_status(400)
187+
self.finish(json.dumps({"message": "Request body is required"}))
188+
return
189+
190+
body = cast("dict[str, str|int]", body)
191+
skip = cast(int, body.get("skip", 0))
192+
limit = cast(int, body.get("limit", 0))
193+
keyword = cast(str, body.get("keyword", ""))
194+
sortField = cast(str, body.get("sortField", "CUSTOM"))
195+
sortOrder = cast(str, body.get("sortOrder", "ASCENDING"))
196+
197+
await self.graphql(
198+
name="question_list",
199+
query={
200+
"query": """query problemsetQuestionListV2($filters: QuestionFilterInput,
201+
$limit: Int,
202+
$searchKeyword: String,
203+
$skip: Int,
204+
$sortBy: QuestionSortByInput,
205+
$categorySlug: String) {
206+
problemsetQuestionListV2(
207+
filters: $filters
208+
limit: $limit
209+
searchKeyword: $searchKeyword
210+
skip: $skip
211+
sortBy: $sortBy
212+
categorySlug: $categorySlug
213+
) {
214+
questions {
215+
id
216+
titleSlug
217+
title
218+
translatedTitle
219+
questionFrontendId
220+
paidOnly
221+
difficulty
222+
topicTags {
223+
name
224+
slug
225+
nameTranslated
226+
}
227+
status
228+
isInMyFavorites
229+
frequency
230+
acRate
231+
}
232+
totalLength
233+
finishedLength
234+
hasMore
235+
}
236+
}""",
237+
"variables": {
238+
"skip": skip,
239+
"limit": limit,
240+
"searchKeyword": keyword,
241+
"categorySlug": "algorithms",
242+
"filters": {
243+
"filterCombineType": "ALL",
244+
"statusFilter": {
245+
"questionStatuses": ["TO_DO"],
246+
"operator": "IS",
247+
},
248+
"difficultyFilter": {
249+
"difficulties": ["MEDIUM", "HARD"],
250+
"operator": "IS",
251+
},
252+
"languageFilter": {"languageSlugs": [], "operator": "IS"},
253+
"topicFilter": {"topicSlugs": [], "operator": "IS"},
254+
"acceptanceFilter": {},
255+
"frequencyFilter": {},
256+
"frontendIdFilter": {},
257+
"lastSubmittedFilter": {},
258+
"publishedFilter": {},
259+
"companyFilter": {"companySlugs": [], "operator": "IS"},
260+
"positionFilter": {"positionSlugs": [], "operator": "IS"},
261+
"premiumFilter": {"premiumStatus": [], "operator": "IS"},
262+
},
263+
"sortBy": {"sortField": sortField, "sortOrder": sortOrder},
264+
},
265+
},
266+
)

src/components/BrowserCookie.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ import React, { useEffect, useState } from 'react';
22
import { getCookie } from '../services/cookie';
33
import Bowser from 'bowser';
44

5-
const BrowserCookie = ({
6-
setCookieLoggedIn
7-
}: {
5+
const BrowserCookie: React.FC<{
86
setCookieLoggedIn: (b: string) => void;
9-
}) => {
7+
}> = ({ setCookieLoggedIn }) => {
108
const browsers = [
119
'Chrome',
1210
'Firefox',

src/components/LandingPage.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import React from 'react';
22
import BrowserCookie from './BrowserCookie';
33

4-
const LandingPage = ({
5-
setCookieLoggedIn
6-
}: {
4+
const LandingPage: React.FC<{
75
setCookieLoggedIn: (b: string) => void;
8-
}) => {
6+
}> = ({ setCookieLoggedIn }) => {
97
const options: JSX.Element[] = [
108
<BrowserCookie setCookieLoggedIn={setCookieLoggedIn} />
119
];

src/components/LeetCode.tsx

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import React, { useEffect, useState } from 'react';
2-
import { getProfile, getStatistics } from '../services/leetcode';
3-
import { LeetCodeProfile, LeetCodeStatistics } from '../types/leetcode';
2+
import { getProfile } from '../services/leetcode';
3+
import { LeetCodeProfile } from '../types/leetcode';
44
import Profile from './Profile';
55
import Statistics from './Statistics';
6+
import QuestionList from './QuestionList';
67

7-
const LeetCode = () => {
8+
const LeetCode: React.FC = () => {
89
const [profile, setProfile] = useState<LeetCodeProfile | null>(null);
9-
const [statistics, setStatistics] = useState<LeetCodeStatistics | null>(null);
1010

1111
useEffect(() => {
1212
getProfile().then(profile => {
@@ -18,19 +18,11 @@ const LeetCode = () => {
1818
});
1919
}, []);
2020

21-
useEffect(() => {
22-
if (!profile) {
23-
return;
24-
}
25-
getStatistics(profile.username).then(d => {
26-
setStatistics(d);
27-
});
28-
}, [profile]);
29-
3021
return profile ? (
3122
<div>
3223
<Profile profile={profile} />
33-
{statistics ? <Statistics statisitcs={statistics} /> : null}
24+
<Statistics username={profile.username} />
25+
<QuestionList />
3426
</div>
3527
) : null;
3628
};

src/components/Profile.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { LeetCodeProfile } from '../types/leetcode';
33

4-
const Profile = ({ profile }: { profile: LeetCodeProfile }) => {
4+
const Profile: React.FC<{ profile: LeetCodeProfile }> = ({ profile }) => {
55
return (
66
<div>
77
<p>Welcome {profile.username}</p>

src/components/QuestionItem.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react';
2+
import { LeetCodeQuestion } from '../types/leetcode';
3+
const QuestionItem: React.FC<{ question: LeetCodeQuestion }> = ({
4+
question
5+
}) => {
6+
return (
7+
<div>
8+
<p>{question.title}</p>
9+
</div>
10+
);
11+
};
12+
13+
export default QuestionItem;

src/components/QuestionList.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { listQuestions } from '../services/leetcode';
3+
import { LeetCodeQuestion } from '../types/leetcode';
4+
import QuestionItem from './QuestionItem';
5+
6+
const QuestionList: React.FC = () => {
7+
const [skip, setSkip] = useState(0);
8+
const limit = 100;
9+
const [keyword, setKeyword] = useState('');
10+
const [questions, setQuestions] = useState<LeetCodeQuestion[]>([]);
11+
const [_hasMore, setHasMore] = useState(true);
12+
const [_finishedLength, setFinishedLength] = useState(0);
13+
const [_totalLength, setTotalLength] = useState(0);
14+
15+
useEffect(() => setSkip(0), [keyword]);
16+
17+
useEffect(() => {
18+
listQuestions(keyword, skip, limit).then(r => {
19+
if (!r) {
20+
return;
21+
}
22+
const {
23+
questions: fetchedQuestions,
24+
hasMore: fetchedHasMore,
25+
finishedLength: fetchedFinishedLength,
26+
totalLength: fetchedTotalLength
27+
} = r.problemsetQuestionListV2;
28+
setQuestions(fetchedQuestions);
29+
setHasMore(fetchedHasMore);
30+
setFinishedLength(fetchedFinishedLength);
31+
setTotalLength(fetchedTotalLength);
32+
});
33+
}, [keyword, skip]);
34+
35+
return (
36+
<div>
37+
<label htmlFor="keyword">Keyword:</label>
38+
<input
39+
type="text"
40+
id="keyword"
41+
value={keyword}
42+
onChange={e => setKeyword(e.target.value)}
43+
/>
44+
{questions.length > 0 ? (
45+
<div>
46+
{questions.map(q => (
47+
<QuestionItem key={q.id} question={q} />
48+
))}
49+
</div>
50+
) : null}
51+
</div>
52+
);
53+
};
54+
55+
export default QuestionList;

src/components/Statistics.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1-
import React from 'react';
1+
import React, { useEffect, useState } from 'react';
2+
import { getStatistics } from '../services/leetcode';
23
import { LeetCodeStatistics } from '../types/leetcode';
34

4-
const Statistics = ({ statisitcs }: { statisitcs: LeetCodeStatistics }) => {
5-
return (
5+
const Statistics: React.FC<{ username: string }> = ({ username }) => {
6+
const [statistics, setStatistics] = useState<LeetCodeStatistics | null>(null);
7+
8+
useEffect(() => {
9+
getStatistics(username).then(d => {
10+
setStatistics(d);
11+
});
12+
}, []);
13+
14+
return statistics ? (
615
<div>
7-
<p>rank: {statisitcs.userPublicProfile.matchedUser.profile.ranking}</p>
16+
<p>rank: {statistics.userPublicProfile.matchedUser.profile.ranking}</p>
817
</div>
9-
);
18+
) : null;
1019
};
1120

1221
export default Statistics;

src/services/leetcode.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,42 @@
1-
import { LeetCodeProfile, LeetCodeStatistics } from '../types/leetcode';
1+
import {
2+
LeetCodeProfile,
3+
LeetCodeQuestion,
4+
LeetCodeStatistics
5+
} from '../types/leetcode';
26
import { requestAPI } from './handler';
37

4-
export async function getProfile(): Promise<LeetCodeProfile | null> {
8+
export async function getProfile() {
59
return requestAPI<{ data: { userStatus: LeetCodeProfile } }>(
610
'/leetcode/profile'
711
)
812
.then(d => d.data.userStatus)
913
.catch(() => null);
1014
}
1115

12-
export async function getStatistics(
13-
username: string
14-
): Promise<LeetCodeStatistics | null> {
16+
export async function getStatistics(username: string) {
1517
return requestAPI<LeetCodeStatistics>(
1618
`/leetcode/statistics?username=${username}`
1719
).catch(() => null);
1820
}
21+
22+
export async function listQuestions(
23+
keyword: string,
24+
skip: number,
25+
limit: number
26+
) {
27+
return requestAPI<{
28+
data: {
29+
problemsetQuestionListV2: {
30+
finishedLength: number;
31+
hasMore: boolean;
32+
totalLength: number;
33+
questions: LeetCodeQuestion[];
34+
};
35+
};
36+
}>('/leetcode/questions', {
37+
method: 'POST',
38+
body: JSON.stringify({ skip, limit, keyword })
39+
})
40+
.then(d => d.data)
41+
.catch(() => null);
42+
}

0 commit comments

Comments
 (0)