Skip to content

Commit 79fc377

Browse files
committed
Add LeetCode profile sync functionality with admin UI and scheduled workflows
1 parent 574ccb2 commit 79fc377

File tree

7 files changed

+665
-13
lines changed

7 files changed

+665
-13
lines changed

.github/workflows/weekly-sync.yml

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Bi-Weekly LeetCode Profile Sync
2+
3+
on:
4+
schedule:
5+
# Run every other Sunday at 00:00 UTC (every 2 weeks)
6+
- cron: "0 0 */14 * *"
7+
8+
# Allow manual triggering
9+
workflow_dispatch:
10+
11+
jobs:
12+
sync-profiles:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Trigger Profile Sync
17+
run: |
18+
curl -X GET "${{ secrets.APP_URL }}/api/syncAllProfiles?secretKey=${{ secrets.SYNC_SECRET_KEY }}" \
19+
-H "Content-Type: application/json" \
20+
--fail
21+
env:
22+
APP_URL: ${{ secrets.APP_URL }}
23+
SYNC_SECRET_KEY: ${{ secrets.SYNC_SECRET_KEY }}
+1
Loading

client/src/app/admin/sync/page.tsx

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"use client";
2+
3+
import React, { useState } from "react";
4+
import Link from "next/link";
5+
import Image from "next/image";
6+
import { useRouter } from "next/navigation";
7+
8+
export default function AdminSync() {
9+
const [isLoading, setIsLoading] = useState(false);
10+
const [results, setResults] = useState<any>(null);
11+
const [error, setError] = useState<string | null>(null);
12+
const [secretKey, setSecretKey] = useState("");
13+
const router = useRouter();
14+
15+
const handleSyncAll = async () => {
16+
if (!secretKey) {
17+
setError("Secret key is required");
18+
return;
19+
}
20+
21+
setIsLoading(true);
22+
setError(null);
23+
24+
try {
25+
const response = await fetch(
26+
`/api/syncAllProfiles?secretKey=${secretKey}`
27+
);
28+
const data = await response.json();
29+
30+
if (!response.ok) {
31+
throw new Error(data.message || "Failed to sync profiles");
32+
}
33+
34+
setResults(data);
35+
} catch (err: any) {
36+
setError(err.message || "An error occurred while syncing profiles");
37+
} finally {
38+
setIsLoading(false);
39+
}
40+
};
41+
42+
return (
43+
<section className="px-4 lg:px-24 py-24 min-h-screen bg-[#0e0e0e] text-white">
44+
<div className="max-w-4xl mx-auto">
45+
<div className="flex items-center mb-8">
46+
<Link href="/" className="mr-4">
47+
<Image
48+
src="/assets/icons/leetcode.svg"
49+
alt="Leetcode Logo"
50+
width={36}
51+
height={36}
52+
/>
53+
</Link>
54+
<h1 className="text-3xl font-bold font-sourcecodepro">
55+
Admin: Profile Sync Tool
56+
</h1>
57+
</div>
58+
59+
<div className="bg-[#1a1a1a] p-6 rounded-lg border border-gray-700 mb-8">
60+
<h2 className="text-xl font-bold mb-4 font-sourcecodepro">
61+
Sync All LeetCode Profiles
62+
</h2>
63+
<p className="mb-6 text-gray-300">
64+
This will update all profiles with the latest data from LeetCode.
65+
This process may take several minutes depending on the number of
66+
profiles.
67+
</p>
68+
69+
<div className="mb-4">
70+
<label className="block text-sm font-medium text-gray-300 mb-2">
71+
Secret Key
72+
</label>
73+
<input
74+
type="password"
75+
value={secretKey}
76+
onChange={(e) => setSecretKey(e.target.value)}
77+
className="w-full p-2 bg-[#0e0e0e] border border-gray-700 rounded text-white"
78+
placeholder="Enter secret key"
79+
/>
80+
{error && <p className="mt-2 text-red-500">{error}</p>}
81+
</div>
82+
83+
<button
84+
onClick={handleSyncAll}
85+
disabled={isLoading}
86+
className={`px-4 py-2 rounded-md font-sourcecodepro font-bold ${
87+
isLoading
88+
? "bg-gray-700 cursor-not-allowed"
89+
: "bg-gradient-to-r from-[#cb42b2] to-[#ecf576] text-black hover:opacity-90"
90+
} transition-all`}
91+
>
92+
{isLoading ? "Syncing..." : "Sync All Profiles"}
93+
</button>
94+
</div>
95+
96+
{results && (
97+
<div className="bg-[#1a1a1a] p-6 rounded-lg border border-gray-700">
98+
<h2 className="text-xl font-bold mb-4 font-sourcecodepro">
99+
Sync Results
100+
</h2>
101+
102+
<div className="flex mb-4 gap-4">
103+
<div className="flex-1 p-4 bg-[#0e0e0e] rounded border border-gray-700">
104+
<div className="text-4xl font-bold mb-2 text-center">
105+
{results.results.total}
106+
</div>
107+
<div className="text-center text-gray-300">Total Profiles</div>
108+
</div>
109+
<div className="flex-1 p-4 bg-[#0e0e0e] rounded border border-gray-700">
110+
<div className="text-4xl font-bold mb-2 text-center text-green-500">
111+
{results.results.successful}
112+
</div>
113+
<div className="text-center text-gray-300">
114+
Successfully Updated
115+
</div>
116+
</div>
117+
<div className="flex-1 p-4 bg-[#0e0e0e] rounded border border-gray-700">
118+
<div className="text-4xl font-bold mb-2 text-center text-red-500">
119+
{results.results.failed}
120+
</div>
121+
<div className="text-center text-gray-300">Failed Updates</div>
122+
</div>
123+
</div>
124+
125+
{results.results.failures.length > 0 && (
126+
<div>
127+
<h3 className="text-lg font-bold mb-2 font-sourcecodepro">
128+
Failed Profiles:
129+
</h3>
130+
<ul className="list-disc pl-5 text-red-400">
131+
{results.results.failures.map(
132+
(username: string, idx: number) => (
133+
<li key={idx}>{username}</li>
134+
)
135+
)}
136+
</ul>
137+
</div>
138+
)}
139+
140+
<div className="mt-6 flex justify-end">
141+
<button
142+
onClick={() => router.push("/")}
143+
className="px-4 py-2 bg-[#0e0e0e] text-white rounded-md border border-gray-700 hover:bg-[#1f1f1f] transition-all"
144+
>
145+
Back to Home
146+
</button>
147+
</div>
148+
</div>
149+
)}
150+
</div>
151+
</section>
152+
);
153+
}

client/src/app/api/cron/route.ts

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
3+
export const config = {
4+
runtime: "edge",
5+
regions: ["iad1"], // specify a single region
6+
};
7+
8+
// This defines a schedule for running once a week (Sunday at 00:00 UTC)
9+
export const dynamic = "force-dynamic";
10+
export const revalidate = 0;
11+
12+
// Set the cron schedule to run monthly (1st day of each month at midnight)
13+
export const maxDuration = 300; // 5 minute maximum duration
14+
export const schedule = { cron: "0 0 1 * *" }; // Monthly at midnight on the 1st day
15+
16+
export async function GET(request: NextRequest) {
17+
try {
18+
const secretKey = process.env.SYNC_SECRET_KEY || "your-default-secret-key";
19+
20+
// Call our sync API
21+
const syncResponse = await fetch(
22+
`${
23+
process.env.NEXT_PUBLIC_BASE_URL || request.nextUrl.origin
24+
}/api/syncAllProfiles?secretKey=${secretKey}`,
25+
{ method: "GET" }
26+
);
27+
28+
const result = await syncResponse.json();
29+
30+
if (!result.success) {
31+
console.error("Sync failed:", result);
32+
return NextResponse.json(
33+
{ message: "Weekly sync failed", error: result },
34+
{ status: 500 }
35+
);
36+
}
37+
38+
return NextResponse.json({
39+
message: "Weekly sync completed successfully",
40+
result,
41+
});
42+
} catch (error) {
43+
console.error("Error during scheduled sync:", error);
44+
return NextResponse.json(
45+
{ message: "Error during weekly sync", error },
46+
{ status: 500 }
47+
);
48+
}
49+
}

client/src/app/page.tsx

+29-13
Original file line numberDiff line numberDiff line change
@@ -112,26 +112,42 @@ export default function Home() {
112112
<GenerateStats showStats={showStats} setShowStats={setShowStats} />
113113

114114
<div className="w-full max-w-7xl mx-auto flex flex-col lg:grid grid-cols-3 items-center justify-start gap-4 lg:gap-12 px-8 mt-28">
115-
<Link
116-
href="/worth"
117-
className="flex w-fit gap-2 rounded border-2 border-[#f7f7f7] px-4 bg-gradient-to-r from-[#cb42b2] to-[#ecf576] bg-clip-text text-transparent p-2 font-sourcecodepro font-bold"
118-
>
119-
<Image
120-
src="/assets/icons/money.svg"
121-
alt="Leetcode Logo"
122-
width={24}
123-
height={24}
124-
/>
125-
LeetCode Worth
126-
</Link>
115+
<div className="flex flex-wrap gap-4">
116+
<Link
117+
href="/worth"
118+
className="flex w-fit gap-2 rounded border-2 border-[#f7f7f7] px-4 bg-gradient-to-r from-[#cb42b2] to-[#ecf576] bg-clip-text text-transparent p-2 font-sourcecodepro font-bold"
119+
>
120+
<Image
121+
src="/assets/icons/money.svg"
122+
alt="Leetcode Logo"
123+
width={24}
124+
height={24}
125+
/>
126+
LeetCode Worth
127+
</Link>
128+
</div>
127129
<select
128130
value={sortBy}
129131
onChange={handleSortChange}
130-
className=" rounded border-2 border-[#f7f7f7] w-64 bg-[#0e0e0e] text-white p-2 font-sourcecodepro"
132+
className="rounded border-2 border-[#f7f7f7] w-64 bg-[#0e0e0e] text-white p-2 font-sourcecodepro"
131133
>
132134
<option value="default">Sort By Default</option>
133135
<option value="question-solved">Sort By Questions Solved</option>
134136
</select>
137+
<div className="flex flex-wrap gap-4">
138+
<Link
139+
href="/admin/sync"
140+
className="flex w-fit gap-2 rounded border-2 border-[#f7f7f7] px-4 bg-gradient-to-r from-[#3b82f6] to-[#22d3ee] bg-clip-text text-transparent p-2 font-sourcecodepro font-bold"
141+
>
142+
<Image
143+
src="/assets/icons/refresh.svg"
144+
alt="Sync Icon"
145+
width={24}
146+
height={24}
147+
/>
148+
Sync Profiles
149+
</Link>
150+
</div>
135151
</div>
136152

137153
<div className="mt-8 max-w-7xl mx-auto place-items-center grid grid-cols-1 md:grid-cols-2 gap-y-8 xl:grid-cols-3 font-sourcecodepro gap-x-4">

0 commit comments

Comments
 (0)