Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add LeetCode Integration #134

Merged
merged 15 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ model User {
spotifyToken SpotifyToken?
RecentActivity RecentActivity[]
IntegrationChessCom IntegrationChessCom?
IntegrationLeetCode IntegrationLeetCode?
CryptoWallets CryptoWallets[]
}

Expand Down Expand Up @@ -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])
Expand Down
37 changes: 37 additions & 0 deletions src/lib/components/MyProfile/LeetCodeForm.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script lang="ts">
import * as Form from '$lib/components/ui/form';
import { Input } from '$lib/components/ui/input';
import { leetCodeSchema, type LeetCodeSchema } from '$lib/schemas/integration-leetcode';
import { type SuperValidated, type Infer, superForm } from 'sveltekit-superforms';
import { zodClient } from 'sveltekit-superforms/adapters';

export let data: SuperValidated<Infer<LeetCodeSchema>>;

const form = superForm(data, {
validators: zodClient(leetCodeSchema),
resetForm: true
});

const { form: formData, enhance } = form;
</script>

<form
method="POST"
use:enhance
action="?/createLeetCode"
class="flex items-center justify-between space-x-4"
>
<div class="flex items-start space-x-2">
<Form.Field {form} name="username">
<Form.Control let:attrs>
<Form.Label>Username</Form.Label>
<Input {...attrs} bind:value={$formData.username} />
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<div class="space-y-2">
<span class="invisible block">a</span>
<Form.Button>Add</Form.Button>
</div>
</div>
</form>
120 changes: 120 additions & 0 deletions src/lib/components/MyProfile/LeetCodeStats.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { LeetCodeStats } from '$lib/types/LeetCodeData';
import { Flame, Calendar, Medal } from 'lucide-svelte';
import * as Card from '$lib/components/ui/card';
import * as Table from '$lib/components/ui/table';

export let leetCodeUsername: string;
let data: LeetCodeStats | null = null;
let loading = true;

// Problem-solving counts by difficulty
let easyCount = 0;
let mediumCount = 0;
let hardCount = 0;

// Fetch data
onMount(async () => {
try {
const response = await fetch(`/api/leetcode?leetCodeUsername=${leetCodeUsername}`);
if (response.ok) {
const result = await response.json();
data = result.data.matchedUser;

// Extract counts for each difficulty
easyCount = data?.submitStatsGlobal.acSubmissionNum.find(item => item.difficulty === "Easy")?.count || 0;
mediumCount = data?.submitStatsGlobal.acSubmissionNum.find(item => item.difficulty === "Medium")?.count || 0;
hardCount = data?.submitStatsGlobal.acSubmissionNum.find(item => item.difficulty === "Hard")?.count || 0;
}
} catch (error) {
console.error('Error fetching LeetCode stats:', error);
} finally {
loading = false;
}
});
</script>

<Card.Root class="max-h-[400px] overflow-y-auto">
<Card.Header>
<Card.Title>LeetCode</Card.Title>
<Card.Description>{leetCodeUsername}'s LeetCode stats</Card.Description>
</Card.Header>

<Card.Content>
{#if loading}
<p>Loading...</p>
{:else if data}
<!-- Problem Solving Summary -->
<h4 class="text-lg font-semibold mt-6 mb-4">Problem Solving Summary</h4>
<Table.Root class="w-full mb-8">
<Table.Header>
<Table.Row>
<Table.Head>Difficulty</Table.Head>
<Table.Head>Count</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>Easy</Table.Cell>
<Table.Cell>{easyCount}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>Medium</Table.Cell>
<Table.Cell>{mediumCount}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>Hard</Table.Cell>
<Table.Cell>{hardCount}</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>

<!-- Badges -->
{#if data.userCalendar.dccBadges.length > 0}
<h4 class="text-lg font-semibold mb-4">Badges</h4>
<div class="badge-grid grid grid-cols-3 gap-4 mb-8">
{#each data.userCalendar.dccBadges as badge}
<div class="badge relative w-12 h-12">
<img src={`https://leetcode.com/${badge.badge.icon}`} alt={badge.badge.name} class="w-full h-auto" />
<div class="tooltip absolute bottom-[-24px] left-1/2 transform -translate-x-1/2 bg-black text-white text-xs rounded px-2 py-1 hidden group-hover:block">
{badge.badge.name}
</div>
</div>
{/each}
</div>
{/if}

<!-- Metrics -->
<h4 class="text-lg font-semibold mb-4">Metrics</h4>
<Table.Root class="w-full">
<Table.Body>
<Table.Row class="flex items-center space-x-4 mb-2">
<Flame class="text-orange-500 w-6 h-6" />
<Table.Cell class="font-medium">Longest Streak:</Table.Cell>
<Table.Cell>{data.userCalendar.streak} days</Table.Cell>
</Table.Row>
<Table.Row class="flex items-center space-x-4 mb-2">
<Calendar class="text-blue-500 w-6 h-6" />
<Table.Cell class="font-medium">Active Days:</Table.Cell>
<Table.Cell>{data.userCalendar.totalActiveDays}</Table.Cell>
</Table.Row>
<Table.Row class="flex items-center space-x-4">
<Medal class="text-yellow-500 w-6 h-6" />
<Table.Cell class="font-medium">Ranking:</Table.Cell>
<Table.Cell>{data.profile.ranking}</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
{:else}
<p>No data available.</p>
{/if}
</Card.Content>
</Card.Root>

<style>
.badge-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(50px, 1fr));
}
</style>
7 changes: 6 additions & 1 deletion src/lib/components/PublicProfile/PublicProfile.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
</script>
Expand All @@ -37,6 +37,11 @@
{#if userData.chessComUsername != null}
<ChessComStats chessComUsername={userData.chessComUsername} />
{/if}

<!-- LeetCode Stats Section -->
{#if userData.leetCode != null}
<LeetCodeStats leetCodeUsername={userData.leetCode.username} />
{/if}
</div>

<div class="flex flex-col items-center justify-center gap-4">
Expand Down
11 changes: 11 additions & 0 deletions src/lib/schemas/integration-leetcode.ts
Original file line number Diff line number Diff line change
@@ -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;



30 changes: 30 additions & 0 deletions src/lib/types/LeetCodeData.ts
Original file line number Diff line number Diff line change
@@ -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;
};
3 changes: 2 additions & 1 deletion src/lib/types/PublicProfile.ts
Original file line number Diff line number Diff line change
@@ -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 }>;
Expand All @@ -10,4 +10,5 @@ export interface PublicProfile {
personalInformation: PersonalInformation | null;
chessComUsername: string | null;
crypto: CryptoWallets[];
leetCode: IntegrationLeetCode | null;
}
4 changes: 3 additions & 1 deletion src/lib/utils/createRecentActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> => {
Expand Down
9 changes: 8 additions & 1 deletion src/routes/[username]/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
56 changes: 56 additions & 0 deletions src/routes/api/leetcode/+server.ts
Original file line number Diff line number Diff line change
@@ -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'
}
});
};
10 changes: 9 additions & 1 deletion src/routes/profile/+layout.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 }
});
Expand All @@ -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 {
Expand All @@ -109,13 +115,15 @@ export const load: LayoutServerLoad = async (event) => {
socials,
spotifyToken,
chessComUsername,
leetCodeUsername,
crypto,
form: linksForm,
skillsForm,
hobbiesForm,
socialsForm,
personalInformationForm,
chessComForm,
cryptoForm
cryptoForm,
leetCodeForm
};
};
Loading
Loading