diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 49cecbd..41fd067 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -29,6 +29,7 @@ model User { spotifyToken SpotifyToken? RecentActivity RecentActivity[] IntegrationChessCom IntegrationChessCom? + IntegrationLeetCode IntegrationLeetCode? CryptoWallets CryptoWallets[] } @@ -108,6 +109,16 @@ model IntegrationChessCom { updatedAt DateTime @updatedAt } +model IntegrationLeetCode { + id Int @id @default(autoincrement()) + usedBy User @relation(fields: [userId], references: [githubId]) + userId Int @unique + username String + visible Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + model CryptoWallets { id Int @id @default(autoincrement()) usedBy User @relation(fields: [userId], references: [githubId]) diff --git a/src/lib/components/MyProfile/LeetCodeForm.svelte b/src/lib/components/MyProfile/LeetCodeForm.svelte new file mode 100644 index 0000000..7750a74 --- /dev/null +++ b/src/lib/components/MyProfile/LeetCodeForm.svelte @@ -0,0 +1,37 @@ + + +
+
+ + + Username + + + + +
+ + Add +
+
+
diff --git a/src/lib/components/MyProfile/LeetCodeStats.svelte b/src/lib/components/MyProfile/LeetCodeStats.svelte new file mode 100644 index 0000000..e17fb16 --- /dev/null +++ b/src/lib/components/MyProfile/LeetCodeStats.svelte @@ -0,0 +1,120 @@ + + + + + LeetCode + {leetCodeUsername}'s LeetCode stats + + + + {#if loading} +

Loading...

+ {:else if data} + +

Problem Solving Summary

+ + + + Difficulty + Count + + + + + Easy + {easyCount} + + + Medium + {mediumCount} + + + Hard + {hardCount} + + + + + + {#if data.userCalendar.dccBadges.length > 0} +

Badges

+
+ {#each data.userCalendar.dccBadges as badge} +
+ {badge.badge.name} + +
+ {/each} +
+ {/if} + + +

Metrics

+ + + + + Longest Streak: + {data.userCalendar.streak} days + + + + Active Days: + {data.userCalendar.totalActiveDays} + + + + Ranking: + {data.profile.ranking} + + + + {:else} +

No data available.

+ {/if} +
+
+ + diff --git a/src/lib/components/PublicProfile/PublicProfile.svelte b/src/lib/components/PublicProfile/PublicProfile.svelte index 350e21e..9e53191 100644 --- a/src/lib/components/PublicProfile/PublicProfile.svelte +++ b/src/lib/components/PublicProfile/PublicProfile.svelte @@ -8,10 +8,10 @@ import ProfileHero from '$lib/components/PublicProfile/ProfileHero.svelte'; import { Separator } from '$lib/components//ui/separator'; import ChessComStats from '$lib/components/MyProfile/ChessComStats.svelte'; + import LeetCodeStats from '$lib/components/MyProfile/LeetCodeStats.svelte'; // Accept userData as a prop export let userData: PublicProfile; - //Accept githubData as a prop export let githubData: GithubData | null; @@ -37,6 +37,11 @@ {#if userData.chessComUsername != null} {/if} + + + {#if userData.leetCode != null} + + {/if}
diff --git a/src/lib/schemas/integration-leetcode.ts b/src/lib/schemas/integration-leetcode.ts new file mode 100644 index 0000000..0b6ea28 --- /dev/null +++ b/src/lib/schemas/integration-leetcode.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; + +export const leetCodeSchema = z.object({ + username: z.string().min(3).max(20) + }); + + +export type LeetCodeSchema = typeof leetCodeSchema; + + + diff --git a/src/lib/types/LeetCodeData.ts b/src/lib/types/LeetCodeData.ts new file mode 100644 index 0000000..55fb79f --- /dev/null +++ b/src/lib/types/LeetCodeData.ts @@ -0,0 +1,30 @@ +export type LeetCodeStats = { + username: string; + userCalendar: { + activeYears: number[]; + streak: number; + totalActiveDays: number; + dccBadges: { + timestamp: string; + badge: { + name: string; + icon: string; + }; + }[]; + }; + profile: { + ranking: number; + }; + submitStatsGlobal:{ + acSubmissionNum: { + difficulty: string; + count: number; + }[]; + } +}; + +export type TagProblem = { + tagName: string; + tagSlug: string; + problemsSolved: number; +}; diff --git a/src/lib/types/PublicProfile.ts b/src/lib/types/PublicProfile.ts index cfe14c5..6e79b0f 100644 --- a/src/lib/types/PublicProfile.ts +++ b/src/lib/types/PublicProfile.ts @@ -1,4 +1,4 @@ -import type { CryptoWallets, PersonalInformation, Social } from '@prisma/client'; +import type { CryptoWallets, PersonalInformation, Social, IntegrationLeetCode} from '@prisma/client'; export interface PublicProfile { links: Array<{ title: string; url: string }>; @@ -10,4 +10,5 @@ export interface PublicProfile { personalInformation: PersonalInformation | null; chessComUsername: string | null; crypto: CryptoWallets[]; + leetCode: IntegrationLeetCode | null; } diff --git a/src/lib/utils/createRecentActivity.ts b/src/lib/utils/createRecentActivity.ts index 83e8a5c..5b7d320 100644 --- a/src/lib/utils/createRecentActivity.ts +++ b/src/lib/utils/createRecentActivity.ts @@ -17,7 +17,9 @@ export const createRecentActivity = async ( | 'CHESS_COM_DELETED' | 'PERSONAL_INFORMATION_UPDATED' | 'CRYPTO_CREATED' - | 'CRYPTO_DELETED', + | 'CRYPTO_DELETED' + | 'LEETCODE_LINKED' + | 'LEETCODE_UNLINKED', activityDescription: string, userId: number ): Promise => { diff --git a/src/routes/[username]/+page.server.ts b/src/routes/[username]/+page.server.ts index bd52064..e1da7ed 100644 --- a/src/routes/[username]/+page.server.ts +++ b/src/routes/[username]/+page.server.ts @@ -60,6 +60,10 @@ export const load: PageServerLoad = async ({ params }) => { const crypto = await prisma.cryptoWallets.findMany({ where: { userId: user.githubId } }); + const leetCode = await prisma.integrationLeetCode.findUnique({ + where: { userId: user.githubId } + }); + const userData: PublicProfile = { links, @@ -70,7 +74,10 @@ export const load: PageServerLoad = async ({ params }) => { username: username, isOpenToCollaborating: isOpenToCollaborating?.openToCollaborating, hobbies, - crypto + crypto, + // TODO add leetCode to the userData + leetCode + }; return { diff --git a/src/routes/api/leetcode/+server.ts b/src/routes/api/leetcode/+server.ts new file mode 100644 index 0000000..83da272 --- /dev/null +++ b/src/routes/api/leetcode/+server.ts @@ -0,0 +1,56 @@ +import type { RequestHandler } from '@sveltejs/kit'; + +export const GET: RequestHandler = async ({ url }) => { + const username = url.searchParams.get('leetCodeUsername'); + const query = ` + query userProfileCalendar($username: String!) { + matchedUser(username: $username) { + username + userCalendar { + activeYears + streak + totalActiveDays + dccBadges { + timestamp + badge { + name + icon + } + } + } + + profile { + ranking + } + + submitStatsGlobal { + acSubmissionNum { + difficulty + count + } + } + } + }`; + const variables = { + username, + }; + + const response = await fetch('https://leetcode.com/graphql', { + headers: { + 'Content-Type': 'application/json' + }, + method: 'POST', + body: JSON.stringify({ + operationName: 'userProfileCalendar', + query, + variables + }) + }); + + const data = await response.json(); + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json' + } + }); +}; \ No newline at end of file diff --git a/src/routes/profile/+layout.server.ts b/src/routes/profile/+layout.server.ts index a8a5f41..c1631d9 100644 --- a/src/routes/profile/+layout.server.ts +++ b/src/routes/profile/+layout.server.ts @@ -13,6 +13,7 @@ import { personalInformationSchema } from '$lib/schemas/personal-information'; import type { LayoutServerLoad } from '../$types'; import { chessComSchema } from '$lib/schemas/integration-chesscom'; import { cryptoSchema } from '$lib/schemas/crypto'; +import { leetCodeSchema } from '$lib/schemas/integration-leetcode'; // Define the user variable with a possible null let user: User | null = null; @@ -78,6 +79,10 @@ export const load: LayoutServerLoad = async (event) => { where: { userId: user.githubId } }); + const leetCodeUsername = await prisma.integrationLeetCode.findFirst({ + where: { userId: user.githubId } + }); + const crypto = await prisma.cryptoWallets.findMany({ where: { userId: user.githubId } }); @@ -97,6 +102,7 @@ export const load: LayoutServerLoad = async (event) => { const personalInformationForm = await superValidate(zod(personalInformationSchema)); const chessComForm = await superValidate(zod(chessComSchema)); const cryptoForm = await superValidate(zod(cryptoSchema)); + const leetCodeForm = await superValidate(zod(leetCodeSchema)); // Return data to the frontend return { @@ -109,6 +115,7 @@ export const load: LayoutServerLoad = async (event) => { socials, spotifyToken, chessComUsername, + leetCodeUsername, crypto, form: linksForm, skillsForm, @@ -116,6 +123,7 @@ export const load: LayoutServerLoad = async (event) => { socialsForm, personalInformationForm, chessComForm, - cryptoForm + cryptoForm, + leetCodeForm }; }; diff --git a/src/routes/profile/+page.svelte b/src/routes/profile/+page.svelte index 4c5524a..3f57f51 100644 --- a/src/routes/profile/+page.svelte +++ b/src/routes/profile/+page.svelte @@ -75,6 +75,16 @@ {/if} + + LeetCode + + {#if data.leetCodeUsername} + + {:else} + Not Linked + {/if} + + diff --git a/src/routes/profile/integrations/+page.server.ts b/src/routes/profile/integrations/+page.server.ts index 8510854..a80d630 100644 --- a/src/routes/profile/integrations/+page.server.ts +++ b/src/routes/profile/integrations/+page.server.ts @@ -9,6 +9,7 @@ import { getGitHubUserIdFromImageUrl } from '$lib/utils/getGithubIDFromImage'; import { createRecentActivity } from '$lib/utils/createRecentActivity'; import { unlinkSpotify } from '$lib/utils/spotify/unlinkSpotify'; import { chessComSchema } from '$lib/schemas/integration-chesscom'; +import { leetCodeSchema } from '$lib/schemas/integration-leetcode'; // Define the user variable with a possible null let user: User | null = null; @@ -23,7 +24,7 @@ export const load: PageServerLoad = async (event) => { // Fetch the user from the database user = await prisma.user.findUnique({ where: { githubId: userId } - }); + }); }; export const actions: Actions = { @@ -156,5 +157,52 @@ export const actions: Actions = { console.log(err); return fail(500, { message: 'Something went wrong.' }); } + }, + + createLeetCode: async (event) => { + const form = await superValidate(event, zod(leetCodeSchema)); + if (!form.valid) return fail(400, { form }); + + const { username } = form.data; + + if (user) { + try { + await prisma.integrationLeetCode.create({ + data: { + username, + userId: user.githubId + } + }); + + createRecentActivity( + 'LEETCODE_LINKED', + `Linked your LeetCode account (${username})`, + user.githubId + ); + } catch (error) { + console.error(error); + throw new Error('Failed to create LeetCode integration'); + } + } + return { form }; + }, + + deleteLeetCode: async ({ url }) => { + try { + if (user) { + await prisma.integrationLeetCode.delete({ + where: { userId: user.githubId } + }); + + createRecentActivity('LEETCODE_UNLINKED', `Unlinked your LeetCode account`, user.githubId); + } + } catch (error) { + console.error(error); + return fail(500, { message: 'Something went wrong.' }); + } } + + + + }; diff --git a/src/routes/profile/integrations/+page.svelte b/src/routes/profile/integrations/+page.svelte index 1a0f2e1..fc61365 100644 --- a/src/routes/profile/integrations/+page.svelte +++ b/src/routes/profile/integrations/+page.svelte @@ -10,7 +10,10 @@ import MusicPlayer from '$lib/components/Shared/MusicPlayer.svelte'; import ChessComForm from '$lib/components/MyProfile/ChessComForm.svelte'; import { IconChess, IconLink } from '@tabler/icons-svelte'; + import { Braces } from 'lucide-svelte'; import ChessComStats from '$lib/components/MyProfile/ChessComStats.svelte'; + import LeetCodeForm from '$lib/components/MyProfile/LeetCodeForm.svelte'; + import LeetCodeStats from '$lib/components/MyProfile/LeetCodeStats.svelte'; export let data: PageData; @@ -42,7 +45,6 @@
-
{#if data.chessComUsername} @@ -108,4 +110,25 @@
+
+ + {#if data.leetCodeUsername} +
+
+ + + +
+ {:else} + + + + {/if} +
+