Skip to content

Commit

Permalink
Merge pull request #134 from s1lvax/dev
Browse files Browse the repository at this point in the history
Add LeetCode Integration
  • Loading branch information
s1lvax authored Dec 2, 2024
2 parents 02b5c31 + 9ebbfc9 commit 2afd468
Show file tree
Hide file tree
Showing 14 changed files with 376 additions and 7 deletions.
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

0 comments on commit 2afd468

Please sign in to comment.