Skip to content

Commit a838750

Browse files
authored
Supabase and leetcode api (#30)
The Supabase and Leetcode api route is added, although leetcode API is not required right now. will be implement later
1 parent 4337cb1 commit a838750

17 files changed

+1299
-13
lines changed

Diff for: app/api/leetcode/route.ts

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { supabase } from '@/lib/supabaseClient';
2+
import axios from 'axios';
3+
import { NextRequest, NextResponse } from 'next/server';
4+
5+
// Fetch LeetCode stats
6+
const fetchLeetCodeStats = async (username: string) => {
7+
const query = `
8+
query getUserProfile($username: String!) {
9+
matchedUser(username: $username) {
10+
username
11+
profile {
12+
realName
13+
ranking
14+
}
15+
submitStats {
16+
acSubmissionNum {
17+
difficulty
18+
count
19+
}
20+
}
21+
}
22+
}
23+
`;
24+
try {
25+
const variables = { username };
26+
const { data } = await axios.post('https://leetcode.com/graphql', {
27+
query,
28+
variables,
29+
});
30+
return data.data.matchedUser;
31+
} catch (error) {
32+
console.error('Error fetching LeetCode data:', error);
33+
return null;
34+
}
35+
};
36+
37+
// Store transformed user stats in Supabase
38+
const storeUserStats = async (id: string, stats: any) => {
39+
const entry = {
40+
id: String(id),
41+
ranking: stats.profile.ranking,
42+
solved_easy: stats.submitStats.acSubmissionNum.find((item: any) => item.difficulty === 'Easy')?.count || "0",
43+
solved_medium: stats.submitStats.acSubmissionNum.find((item: any) => item.difficulty === 'Medium')?.count || "0",
44+
solved_hard: stats.submitStats.acSubmissionNum.find((item: any) => item.difficulty === 'Hard')?.count || "0",
45+
};
46+
const { data, error } = await supabase.from('user_info').upsert([entry]);
47+
48+
if (error) {
49+
console.error('Error storing data in Supabase:', error);
50+
}
51+
52+
return data;
53+
};
54+
55+
// Transform LeetCode data into a UI-friendly structure
56+
const transformLeetCodeData = (stats: any) => {
57+
return {
58+
username: stats.username,
59+
profile: {
60+
realName: stats.profile.realName || "Unknown",
61+
ranking: stats.profile.ranking?.toString() || "0",
62+
},
63+
submitStats: {
64+
acSubmissionNum: stats.submitStats.acSubmissionNum.map((item: any) => ({
65+
difficulty: item.difficulty,
66+
count: item.count?.toString() || "0",
67+
})),
68+
},
69+
};
70+
};
71+
72+
// API POST Handler
73+
export async function POST(req: NextRequest) {
74+
const searchParams = req.nextUrl.searchParams;
75+
const username = searchParams.get('username');
76+
const id = searchParams.get('id');
77+
78+
if (!username || !id) {
79+
return NextResponse.json({ error: "Username and id are required" }, { status: 400 });
80+
}
81+
82+
const stats = await fetchLeetCodeStats(username);
83+
84+
if (!stats) {
85+
return NextResponse.json({ error: "User not found" }, { status: 404 });
86+
}
87+
88+
const transformedStats = transformLeetCodeData(stats);
89+
90+
await storeUserStats(id, transformedStats);
91+
92+
return NextResponse.json({ message: "Success", stats: transformedStats });
93+
}

Diff for: app/dashboard/page.tsx

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"use client"
2+
import { useEffect, useState } from 'react';
3+
import { supabase } from '@/lib/supabaseClient';
4+
import { useRouter } from 'next/navigation';
5+
import Navbar from '@/components/header';
6+
import StatsCard from '@/components/Stats';
7+
import { fetchLeetCodeStats } from '@/lib/utils';
8+
9+
export default function Dashboard() {
10+
const [userData, setUserData] = useState<any>(null);
11+
const [loading, setLoading] = useState(true);
12+
const [error, setError] = useState<string | null>(null);
13+
const router = useRouter();
14+
15+
useEffect(() => {
16+
const fetchData = async () => {
17+
try {
18+
const { data, error } = await supabase.auth.getSession();
19+
20+
if (error) throw new Error("Error fetching session.");
21+
22+
const session = data.session;
23+
if (!session) {
24+
router.push('/login');
25+
return;
26+
}
27+
// Fetch user-specific data in a single call
28+
const { data: userInfo, error: userInfoError } = await supabase
29+
.from('user_info')
30+
.select('*')
31+
.eq('user_id', session.user.id)
32+
.single();
33+
34+
if (userInfoError) throw userInfoError;
35+
36+
setUserData(userInfo);
37+
38+
} catch (err: any) {
39+
console.error(err);
40+
setError(err.message || 'An error occurred.');
41+
router.push('/login');
42+
} finally {
43+
setLoading(false);
44+
}
45+
};
46+
47+
fetchData();
48+
}, [router]);
49+
50+
if (loading) return <p>Loading...</p>;
51+
52+
if (error) {
53+
return (
54+
<div>
55+
<Navbar userId={userData.user_id} />
56+
<p className="text-red-500">{error}</p>
57+
</div>
58+
);
59+
}
60+
61+
return (
62+
<div>
63+
<Navbar userId={userData?.user_id} />
64+
<div className="container mx-auto p-4">
65+
<h1 className="text-xl font-bold mb-4">Welcome, {userData.name}</h1>
66+
<div className="mb-4">
67+
<p>LeetCode Username: {userData.leetcode_username}</p>
68+
<p>Gender: {userData.gender}</p>
69+
</div>
70+
71+
<div className="mt-6">
72+
<h2 className="text-lg font-bold mb-2">LeetCode Stats</h2>
73+
<StatsCard leetcodeUsername={userData.leetcode_username} id={userData.id} />
74+
</div>
75+
</div>
76+
</div>
77+
);
78+
}

Diff for: app/login/page.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import LoginForm from "@/components/LoginForm";
2+
3+
export default function SignupPage() {
4+
return (
5+
<div className="">
6+
<LoginForm />
7+
</div>
8+
);
9+
}

Diff for: app/signup/page.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import SignupForm from "@/components/SignupForm";
2+
3+
export default function SignupPage() {
4+
return (
5+
<div className="">
6+
<SignupForm />
7+
</div>
8+
);
9+
}

Diff for: components/LoginForm.tsx

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
'use client';
2+
3+
import React, { useState } from 'react';
4+
import { Button } from '@/components/ui/button';
5+
import { Input } from '@/components/ui/input';
6+
import { Label } from '@/components/ui/label';
7+
import { useRouter } from 'next/navigation';
8+
import { supabase } from '@/lib/supabaseClient';
9+
import Link from 'next/link';
10+
11+
const LoginForm: React.FC = () => {
12+
const router = useRouter();
13+
const [formData, setFormData] = useState<{ email: string; password: string }>({
14+
email: '',
15+
password: '',
16+
});
17+
18+
const [loading, setLoading] = useState(false);
19+
const [error, setError] = useState<string | null>(null);
20+
21+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
22+
const { name, value } = e.target;
23+
setFormData((prev) => ({ ...prev, [name]: value }));
24+
};
25+
26+
const handleSubmit = async (e: React.FormEvent) => {
27+
e.preventDefault();
28+
setLoading(true);
29+
setError(null);
30+
31+
const { email, password } = formData;
32+
33+
try {
34+
// Attempt user login
35+
const { data, error: loginError } = await supabase.auth.signInWithPassword({ email, password });
36+
37+
if (loginError) {
38+
throw new Error(loginError.message);
39+
}
40+
41+
// Redirect to dashboard if login succeeds
42+
if (data.session) {
43+
router.push('/dashboard');
44+
} else {
45+
throw new Error('Unable to retrieve session after login.');
46+
}
47+
} catch (err: any) {
48+
console.error('Login Error:', err);
49+
setError(err.message || 'Something went wrong.');
50+
} finally {
51+
setLoading(false);
52+
}
53+
};
54+
55+
return (
56+
<main className="w-full h-screen flex flex-col items-center justify-center px-4">
57+
<div className="max-w-sm w-full text-gray-600">
58+
<div className="p-6 rounded-lg shadow-lg ">
59+
<h2 className="text-2xl font-bold mb-4">Log In</h2>
60+
<form onSubmit={handleSubmit}>
61+
{/* Email Field */}
62+
<div className="mb-4">
63+
<Label htmlFor="email">Email</Label>
64+
<Input
65+
id="email"
66+
name="email"
67+
type="email"
68+
placeholder="[email protected]"
69+
value={formData.email}
70+
onChange={handleChange}
71+
required
72+
/>
73+
</div>
74+
75+
{/* Password Field */}
76+
<div className="mb-4">
77+
<Label htmlFor="password">Password</Label>
78+
<Input
79+
id="password"
80+
name="password"
81+
type="password"
82+
placeholder="********"
83+
value={formData.password}
84+
onChange={handleChange}
85+
required
86+
/>
87+
</div>
88+
89+
{/* Error Message */}
90+
{error && <p className="text-red-500 mb-4">{error}</p>}
91+
92+
{/* Submit Button */}
93+
<Button type="submit" disabled={loading} className="w-full">
94+
{loading ? 'Logging in...' : 'Log In'}
95+
</Button>
96+
97+
<div className="flex justify-center mt-4">
98+
<Link href="/signup" className="text-sm text-gray-500">
99+
Don't have an account?
100+
<span className='hover:underline ms-1'>Sign up</span>
101+
</Link>
102+
</div>
103+
</form>
104+
</div>
105+
</div>
106+
</main>
107+
);
108+
};
109+
110+
export default LoginForm;

0 commit comments

Comments
 (0)