From e6f8e542af19b5d2bb540c0d5c4c67101c67d0d9 Mon Sep 17 00:00:00 2001 From: momo-shogun Date: Wed, 8 Jan 2025 06:25:05 +0530 Subject: [PATCH 1/5] showing leetcode information --- app/api/leetcode/route.ts | 95 ++++++ app/api/storeUserStats/route.ts | 41 +++ app/dashboard/page.tsx | 84 +++++ app/login/page.tsx | 9 + app/signup/page.tsx | 9 + components/LoginForm.tsx | 102 +++++++ components/SignupForm.tsx | 189 ++++++++++++ components/Stats.tsx | 85 ++++++ components/header.tsx | 8 +- components/ui/badge.tsx | 36 +++ components/ui/input.tsx | 22 ++ components/ui/label.tsx | 26 ++ components/ui/select.tsx | 159 ++++++++++ lib/supabaseClient.ts | 6 + lib/utils.ts | 11 + middleware.ts | 17 ++ package-lock.json | 521 +++++++++++++++++++++++++------- package.json | 5 + 18 files changed, 1310 insertions(+), 115 deletions(-) create mode 100644 app/api/leetcode/route.ts create mode 100644 app/api/storeUserStats/route.ts create mode 100644 app/dashboard/page.tsx create mode 100644 app/login/page.tsx create mode 100644 app/signup/page.tsx create mode 100644 components/LoginForm.tsx create mode 100644 components/SignupForm.tsx create mode 100644 components/Stats.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/select.tsx create mode 100644 lib/supabaseClient.ts create mode 100644 middleware.ts diff --git a/app/api/leetcode/route.ts b/app/api/leetcode/route.ts new file mode 100644 index 0000000..c45a84e --- /dev/null +++ b/app/api/leetcode/route.ts @@ -0,0 +1,95 @@ +import { supabase } from '@/lib/supabaseClient'; +import axios from 'axios'; +import { NextRequest, NextResponse } from 'next/server'; + +// Fetch LeetCode stats +const fetchLeetCodeStats = async (username: string) => { + const query = ` + query getUserProfile($username: String!) { + matchedUser(username: $username) { + username + profile { + realName + ranking + } + submitStats { + acSubmissionNum { + difficulty + count + } + } + } + } + `; + try { + const variables = { username }; + const { data } = await axios.post('https://leetcode.com/graphql', { + query, + variables, + }); + return data.data.matchedUser; + } catch (error) { + console.error('Error fetching LeetCode data:', error); + return null; + } +}; + +// Store transformed user stats in Supabase +const storeUserStats = async (userId: string, stats: any) => { + const entry = { + user_id: String(userId), + ranking: stats.profile.ranking, + solved_easy: stats.submitStats.acSubmissionNum.find((item: any) => item.difficulty === 'Easy')?.count || "0", + solved_medium: stats.submitStats.acSubmissionNum.find((item: any) => item.difficulty === 'Medium')?.count || "0", + solved_hard: stats.submitStats.acSubmissionNum.find((item: any) => item.difficulty === 'Hard')?.count || "0", + }; + const { data, error } = await supabase.from('user_info').upsert([entry]); + + if (error) { + console.error('Error storing data in Supabase:', error); + } + + return data; +}; + +// Transform LeetCode data into a UI-friendly structure +const transformLeetCodeData = (stats: any) => { + return { + username: stats.username, + profile: { + realName: stats.profile.realName || "Unknown", + ranking: stats.profile.ranking?.toString() || "0", + }, + submitStats: { + acSubmissionNum: stats.submitStats.acSubmissionNum.map((item: any) => ({ + difficulty: item.difficulty, + count: item.count?.toString() || "0", + })), + }, + }; +}; + +// API POST Handler +export async function POST(req: NextRequest, res: NextResponse) { + const searchParams = req.nextUrl.searchParams; + const username = searchParams.get('username'); + const userId = searchParams.get('userId'); + + if (!username || !userId) { + return NextResponse.json({ error: "Username and userId are required" }, { status: 400 }); + } + + const stats = await fetchLeetCodeStats(username); + + if (!stats) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + const transformedStats = transformLeetCodeData(stats); + + console.log('Transformed Stats:', JSON.stringify(transformedStats, null, 2)); + + await storeUserStats(userId, transformedStats); + + return NextResponse.json({ message: "Success", stats:transformedStats }); +} diff --git a/app/api/storeUserStats/route.ts b/app/api/storeUserStats/route.ts new file mode 100644 index 0000000..3464b80 --- /dev/null +++ b/app/api/storeUserStats/route.ts @@ -0,0 +1,41 @@ +import { supabase } from '@/lib/supabaseClient'; +import { NextRequest, NextResponse } from 'next/server'; + +const storeUserStats = async (userId: string, stats: any) => { + const { data, error } = await supabase.from('user_info').upsert([ + { + user_id: userId, + name: stats.profile.realName, + email: stats.profile.realName, + leetcode_username: stats.username, + ranking: stats.profile.ranking, + solved_easy: stats.submitStats.acSubmissionNum[0].count, + solved_medium: stats.submitStats.acSubmissionNum[1].count, + solved_hard: stats.submitStats.acSubmissionNum[2].count, + total_contests: stats.contestRanking.attendedContestsCount || 0, + rating: stats.contestRanking.rating || 0, + }, + ]); + + if (error) { + console.error('Error storing data in Supabase:', error); + } + + return data; +}; + +export async function POST(req: NextRequest) { + const { userId, stats } = await req.json(); + + if (!userId || !stats) { + return NextResponse.json({ error: "User ID and stats are required" }, { status: 400 }); + } + + const data = await storeUserStats(userId, stats); + + if (!data) { + return NextResponse.json({ error: "Error storing data in Supabase" }, { status: 500 }); + } + + return NextResponse.json({ message: "Data successfully stored in Supabase" }, { status: 200 }); +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000..56775dd --- /dev/null +++ b/app/dashboard/page.tsx @@ -0,0 +1,84 @@ +"use client" +import { useEffect, useState } from 'react'; +import { supabase } from '@/lib/supabaseClient'; +import { useRouter } from 'next/navigation'; +import Navbar from '@/components/header'; +import StatsCard from '@/components/Stats'; +import { fetchLeetCodeStats } from '@/lib/utils'; + +export default function Dashboard() { + const [userData, setUserData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const router = useRouter(); + + useEffect(() => { + const fetchData = async () => { + try { + const { data, error } = await supabase.auth.getSession(); + + if (error) throw new Error("Error fetching session."); + + const session = data.session; + if (!session) { + router.push('/login'); + return; + } + // Fetch user-specific data in a single call + const { data: userInfo, error: userInfoError } = await supabase + .from('user_info') + .select('*') + .eq('user_id', session.user.id) + .single(); + + if (userInfoError) throw userInfoError; + + setUserData(userInfo); + + } catch (err: any) { + console.error(err); + setError(err.message || 'An error occurred.'); + router.push('/login'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [router]); + + if (loading) return

Loading...

; + + if (error) { + return ( +
+ +

{error}

+
+ ); + } + + return ( +
+ +
+

Welcome, {userData.name}

+
+

LeetCode Username: {userData.leetcode_username}

+

Gender: {userData.gender}

+

User ID: {userData.user_id}

+
+ +
+

LeetCode Stats

+ +
+
+
+ ); +} diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..14af8d5 --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,9 @@ +import LoginForm from "@/components/LoginForm"; + +export default function SignupPage() { + return ( +
+ +
+ ); +} diff --git a/app/signup/page.tsx b/app/signup/page.tsx new file mode 100644 index 0000000..22d0a70 --- /dev/null +++ b/app/signup/page.tsx @@ -0,0 +1,9 @@ +import SignupForm from "@/components/SignupForm"; + +export default function SignupPage() { + return ( +
+ +
+ ); +} diff --git a/components/LoginForm.tsx b/components/LoginForm.tsx new file mode 100644 index 0000000..d859a6c --- /dev/null +++ b/components/LoginForm.tsx @@ -0,0 +1,102 @@ +'use client'; + +import React, { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useRouter } from 'next/navigation'; +import { supabase } from '@/lib/supabaseClient'; + +const LoginForm: React.FC = () => { + const router = useRouter(); + const [formData, setFormData] = useState<{ email: string; password: string }>({ + email: '', + password: '', + }); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + + const { email, password } = formData; + + try { + // Attempt user login + const { data, error: loginError } = await supabase.auth.signInWithPassword({ email, password }); + + if (loginError) { + throw new Error(loginError.message); + } + + // Redirect to dashboard if login succeeds + if (data.session) { + router.push('/dashboard'); + } else { + throw new Error('Unable to retrieve session after login.'); + } + } catch (err: any) { + console.error('Login Error:', err); + setError(err.message || 'Something went wrong.'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+

Log In

+
+ {/* Email Field */} +
+ + +
+ + {/* Password Field */} +
+ + +
+ + {/* Error Message */} + {error &&

{error}

} + + {/* Submit Button */} + +
+
+
+
+ ); +}; + +export default LoginForm; diff --git a/components/SignupForm.tsx b/components/SignupForm.tsx new file mode 100644 index 0000000..b1a0414 --- /dev/null +++ b/components/SignupForm.tsx @@ -0,0 +1,189 @@ +'use client'; + +import React, { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useRouter } from 'next/navigation'; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { supabase } from '@/lib/supabaseClient'; + +const SignupForm: React.FC = () => { + const router = useRouter(); + const [formData, setFormData] = useState<{ + name: string; + email: string; + password: string; + leetcodeUsername: string; + gender: string; + }>({ + name: '', + email: '', + password: '', + leetcodeUsername: '', + gender: '', + }); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const handleSelectChange = (value: string) => { + setFormData({ ...formData, gender: value }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + + try { + const { email, password, name, leetcodeUsername, gender } = formData; + + // Sign up the user + const { data: signUpData, error: signUpError } = await supabase.auth.signUp({ + email, + password, + + }); + + if (signUpError) { + throw new Error(signUpError.message); + } + + const userId = signUpData?.user?.id; + + // Insert additional user information + if (userId) { + const { error: insertError } = await supabase.from('user_info').insert([ + { + user_id: userId, + name, + leetcode_username: leetcodeUsername, + gender, + }, + ]); + + if (insertError) { + throw new Error(insertError.message); + } + } else { + throw new Error('User ID not found. Please try again.'); + } + + // Redirect to the dashboard + router.push('/dashboard'); + } catch (err: any) { + console.error('Error:', err); + setError(err.message || 'Something went wrong. Please try again.'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+

Sign Up

+
+ {/* Name Field */} +
+ + +
+ + {/* Email Field */} +
+ + +
+ + {/* Password Field */} +
+ + +
+ + {/* LeetCode Username Field */} +
+ + +
+ + {/* Gender Field */} +
+ + +
+ + {/* Error Message */} + {error &&

{error}

} + + {/* Submit Button */} + +
+
+
+
+ ); +}; + +export default SignupForm; diff --git a/components/Stats.tsx b/components/Stats.tsx new file mode 100644 index 0000000..e684084 --- /dev/null +++ b/components/Stats.tsx @@ -0,0 +1,85 @@ +import { useState, useEffect } from 'react'; +import { Card } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { fetchLeetCodeStats } from '@/lib/utils'; +import { supabase } from '@/lib/supabaseClient'; + +interface StatsCardProps { + leetcodeUsername: string; + userId:string +} + +const StatsCard: React.FC = ({ leetcodeUsername, userId }) => { + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + + useEffect(() => { + const fetchStats = async () => { + try { + const {stats} = await fetchLeetCodeStats(leetcodeUsername, userId); + setStats(stats); + } catch (err: any) { + setError(err.message || 'Error fetching LeetCode stats.'); + } finally { + setLoading(false); + } + }; + console.log(stats); + + fetchStats(); + }, [leetcodeUsername]); + + if (loading) { + return ( + +

Loading stats...

+
+ ); + } + + if (error) { + return ( + +

{error}

+
+ ); + } + + if (!stats) { + return null; + } + + const { profile, submitStats } = stats; + const ranking = profile?.ranking || 'N/A'; + const solvedEasy = submitStats?.acSubmissionNum?.find((item: any) => item.difficulty === 'Easy')?.count || 0; + const solvedMedium = submitStats?.acSubmissionNum?.find((item: any) => item.difficulty === 'Medium')?.count || 0; + const solvedHard = submitStats?.acSubmissionNum?.find((item: any) => item.difficulty === 'Hard')?.count || 0; + + + return ( + +
+
+

Ranking:

+ {ranking} +
+
+

Solved Easy:

+ {solvedEasy} +
+
+

Solved Medium:

+ {solvedMedium} +
+
+

Solved Hard:

+ {solvedHard} +
+
+
+ ); +}; + +export default StatsCard; diff --git a/components/header.tsx b/components/header.tsx index 083d97f..486d85a 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -33,10 +33,14 @@ const Navbar: FC = () => {
- + Log in - +
+

LeetCode Stats

- +
diff --git a/components/LoginForm.tsx b/components/LoginForm.tsx index d859a6c..c95b83f 100644 --- a/components/LoginForm.tsx +++ b/components/LoginForm.tsx @@ -6,6 +6,7 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useRouter } from 'next/navigation'; import { supabase } from '@/lib/supabaseClient'; +import Link from 'next/link'; const LoginForm: React.FC = () => { const router = useRouter(); @@ -92,6 +93,13 @@ const LoginForm: React.FC = () => { + +
+ + Don't have an account? + Sign up + +
diff --git a/components/SignupForm.tsx b/components/SignupForm.tsx index b1a0414..dac240a 100644 --- a/components/SignupForm.tsx +++ b/components/SignupForm.tsx @@ -15,6 +15,7 @@ import { SelectValue, } from '@/components/ui/select'; import { supabase } from '@/lib/supabaseClient'; +import Link from 'next/link'; const SignupForm: React.FC = () => { const router = useRouter(); @@ -179,6 +180,12 @@ const SignupForm: React.FC = () => { +
+ + Already have an account? + Log in + +
diff --git a/components/Stats.tsx b/components/Stats.tsx index e684084..d71ed47 100644 --- a/components/Stats.tsx +++ b/components/Stats.tsx @@ -2,14 +2,13 @@ import { useState, useEffect } from 'react'; import { Card } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { fetchLeetCodeStats } from '@/lib/utils'; -import { supabase } from '@/lib/supabaseClient'; interface StatsCardProps { leetcodeUsername: string; - userId:string + id:string } -const StatsCard: React.FC = ({ leetcodeUsername, userId }) => { +const StatsCard: React.FC = ({ leetcodeUsername, id }) => { const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -18,7 +17,7 @@ const StatsCard: React.FC = ({ leetcodeUsername, userId }) => { useEffect(() => { const fetchStats = async () => { try { - const {stats} = await fetchLeetCodeStats(leetcodeUsername, userId); + const {stats} = await fetchLeetCodeStats(leetcodeUsername, id); setStats(stats); } catch (err: any) { setError(err.message || 'Error fetching LeetCode stats.'); @@ -26,7 +25,6 @@ const StatsCard: React.FC = ({ leetcodeUsername, userId }) => { setLoading(false); } }; - console.log(stats); fetchStats(); }, [leetcodeUsername]); diff --git a/components/header.tsx b/components/header.tsx index 486d85a..fb1b3e9 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -1,19 +1,21 @@ -'use client' -import { FC, useState } from "react"; -import Link from "next/link"; +import { useState } from "react"; +import Link from "next/link"; import { Button } from "@/components/ui/button"; import { ThemeToggle } from "@/components/theme-toggle"; +import { supabase } from "@/lib/supabaseClient"; +import { useRouter } from "next/router"; -const Navbar: FC = () => { +const Navbar = ({ userId }: { userId?: string }) => { const [isMenuOpen, setIsMenuOpen] = useState(false); - + const router = useRouter(); return (
- - {isMenuOpen && (