algocoach 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,84 @@
1
+ const LEETCODE_API = "https://leetcode.com/graphql";
2
+
3
+ interface LeetCodeProblem {
4
+ title: string
5
+ titleSlug: string
6
+ difficulty: "Easy" | "Medium" | "Hard"
7
+ frontendQuestionId: string
8
+ topicTags: { name: string; slug: string }[]
9
+ acRate: number
10
+ paidOnly: boolean
11
+ }
12
+
13
+ interface LeetCodeSearchResult {
14
+ problemsetQuestionList: {
15
+ total: number
16
+ questions: LeetCodeProblem[]
17
+ }
18
+ }
19
+
20
+ const searchQuery = `
21
+ query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) {
22
+ problemsetQuestionList: questionList(
23
+ categorySlug: $categorySlug
24
+ limit: $limit
25
+ skip: $skip
26
+ filters: $filters
27
+ ) {
28
+ total: totalNum
29
+ questions: data {
30
+ acRate
31
+ difficulty
32
+ frontendQuestionId: questionFrontendId
33
+ paidOnly: isPaidOnly
34
+ title
35
+ titleSlug
36
+ topicTags {
37
+ name
38
+ slug
39
+ }
40
+ }
41
+ }
42
+ }
43
+ `;
44
+
45
+ export async function searchLeetCodeProblems(params: {
46
+ topics?: string[]
47
+ difficulty?: "EASY" | "MEDIUM" | "HARD"
48
+ limit?: number
49
+ excludeSlugs?: string[]
50
+ }): Promise<LeetCodeProblem[]> {
51
+ const { topics, difficulty, limit = 15, excludeSlugs = [] } = params
52
+
53
+ const filters: Record<string, unknown> = {}
54
+ if (topics?.length) filters.tags = topics
55
+ if (difficulty) filters.difficulty = difficulty
56
+
57
+ const res = await fetch(LEETCODE_API, {
58
+ method: "POST",
59
+ headers: { "Content-Type": "application/json" },
60
+ body: JSON.stringify({
61
+ query: searchQuery,
62
+ variables: {
63
+ categorySlug: "",
64
+ limit: limit + excludeSlugs.length,
65
+ skip: 0,
66
+ filters,
67
+ },
68
+ }),
69
+ })
70
+
71
+ const json = await res.json() as { data?: LeetCodeSearchResult; errors?: { message: string }[] }
72
+ if (json.errors?.length) {
73
+ throw new Error(json.errors[0].message || "LeetCode search API error")
74
+ }
75
+
76
+ let problems = json.data?.problemsetQuestionList?.questions || []
77
+ problems = problems.filter((p) => !p.paidOnly)
78
+
79
+ if (excludeSlugs.length) {
80
+ problems = problems.filter((p) => !excludeSlugs.includes(p.titleSlug))
81
+ }
82
+
83
+ return problems.slice(0, limit)
84
+ }
@@ -0,0 +1,84 @@
1
+ const LEETCODE_API = "https://leetcode.com/graphql";
2
+
3
+ const getLeetcodeDataQuery = `
4
+ query GetLeetCodeProfile($username: String!) {
5
+ matchedUser(username: $username) {
6
+ username
7
+ submitStats {
8
+ acSubmissionNum {
9
+ difficulty
10
+ count
11
+ }
12
+ }
13
+ }
14
+ userContestRanking(username: $username) {
15
+ rating
16
+ globalRanking
17
+ }
18
+ }
19
+ `;
20
+
21
+ export interface LeetCodeStats {
22
+ username: string
23
+ totalSolved: number
24
+ easySolved: number
25
+ mediumSolved: number
26
+ hardSolved: number
27
+ rating: number | null
28
+ ranking: number | null
29
+ }
30
+
31
+ async function graphqlRequest<T>(query: string, variables: Record<string, unknown>): Promise<T> {
32
+ const res = await fetch(LEETCODE_API, {
33
+ method: "POST",
34
+ headers: { "Content-Type": "application/json" },
35
+ body: JSON.stringify({ query, variables }),
36
+ })
37
+ const json = await res.json() as { data: T; errors?: { message: string }[] }
38
+ if (json.errors?.length) {
39
+ throw new Error(json.errors[0].message || "LeetCode API error")
40
+ }
41
+ return json.data
42
+ }
43
+
44
+ export async function validateUsername(username: string): Promise<boolean> {
45
+ try {
46
+ const data = await graphqlRequest<{ matchedUser: { username: string } | null }>(
47
+ `query ($username: String!) { matchedUser(username: $username) { username } }`,
48
+ { username },
49
+ )
50
+ return data.matchedUser !== null
51
+ } catch {
52
+ return false
53
+ }
54
+ }
55
+
56
+ export async function fetchLeetcodeStats(username: string): Promise<LeetCodeStats> {
57
+ const valid = await validateUsername(username)
58
+ if (!valid) throw new Error("LeetCode username not found")
59
+
60
+ const data = await graphqlRequest<{
61
+ matchedUser: {
62
+ username: string
63
+ submitStats: {
64
+ acSubmissionNum: { difficulty: string; count: number }[]
65
+ }
66
+ }
67
+ userContestRanking: { rating: number; globalRanking: number } | null
68
+ }>(getLeetcodeDataQuery, { username })
69
+
70
+ const counts: Record<string, number> = {}
71
+ for (const entry of data.matchedUser.submitStats.acSubmissionNum) {
72
+ counts[entry.difficulty.toLowerCase()] = entry.count
73
+ }
74
+
75
+ return {
76
+ username: data.matchedUser.username,
77
+ totalSolved: counts.all || 0,
78
+ easySolved: counts.easy || 0,
79
+ mediumSolved: counts.medium || 0,
80
+ hardSolved: counts.hard || 0,
81
+ rating: data.userContestRanking?.rating ?? null,
82
+ ranking: data.userContestRanking?.globalRanking ?? null,
83
+ }
84
+ }