diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..818b354 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# GITHUB_ID= +# GITHUB_SECRET= +# NEXTAUTH_SECRET= +# POSTGRES_URL= +# SUPABASE_KEY= \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29f0fcd --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts +bun.lockb +package-lock.json diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..af3c921 --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,30 @@ +import NextAuth, { NextAuthOptions, Session } from "next-auth" +import { JWT } from "next-auth/jwt"; +import GithubProvider from "next-auth/providers/github" + +const authOptions: NextAuthOptions = { + // Configure one or more authentication providers + providers: [ + GithubProvider({ + clientId: process.env.GITHUB_ID!, + clientSecret: process.env.GITHUB_SECRET!, + }), + ], + secret: process.env.NEXTAUTH_SECRET, + callbacks: { + async session({ session, token }: { session: any; token: JWT }) { + session.accessToken = token.accessToken as string; + return session; + }, + async jwt({ token, account }: { token: JWT; account: any }) { + if (account) { + token.accessToken = account.access_token as string; + } + return token; + }, + }, +} + +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST } \ No newline at end of file diff --git a/app/api/contributors/route.ts b/app/api/contributors/route.ts new file mode 100644 index 0000000..0d55680 --- /dev/null +++ b/app/api/contributors/route.ts @@ -0,0 +1,65 @@ +import { fetchContributors, fetchUserData } from '../../../utils/fetchData'; +import { supabase } from '@/utils/supabase'; + +export const GET = async (req: any, res: any) => { + try { + const data = await fetchContributions(); + return Response.json(data); + } catch (error) { + console.error(error) + return Response.json({ error: 'Internal Server Error' }); + } +} + + +async function fetchContributions() { + + let { data, error } = await supabase + .from('contributions') + .select('*') + + if(error){ + console.error('error', error); + return []; + // throw error + } + + // data should be { username: username, totalPRs: totalPrs, mergedPRs: mergedPRs, openPRs: openPRs, issues: issueCount, avatar:avatar_url }; + console.log(data) + return data; +} + +export const POST = async (req: any, res: any) => { + try { + + //get access_token from req, validate it and add user to Users table + const json = await req.json(); + let { data: { access_token: access_token } } = json + + const { username: username, avatar: avatar }: { username: string; avatar: string } = await fetchUserData(access_token); + + console.log("username:", username, "avatar:", avatar); + + if (!(username && avatar)) { + throw Error("Invalid username or avatar"); + } + + // new user + // insert into Users table + + const { data, error } = await supabase + .from('Users') + .insert([ + { username: username, avatar: avatar } + ]) + .select() + + console.log("data:", data, "error:", error) + return Response.json({ data: "User added successfully" }); + + } catch (error) { + console.error(error); + return Response.json({ error: 'Internal Server Error' }); + } + +} diff --git a/app/api/refreshDB/route.ts b/app/api/refreshDB/route.ts new file mode 100644 index 0000000..01a1ae1 --- /dev/null +++ b/app/api/refreshDB/route.ts @@ -0,0 +1,96 @@ +import { fetchContributionData } from "@/utils/fetchData"; +import { supabase } from "@/utils/supabase" + +export const GET = async (req: any, res: any) => { + + let contributorsData: { username: string; totalPRs: number; mergedPRs: number; openPRs: number; issues: number; avatar?: string }[] = [] + + // cron.schedule('* * * * *', async () => { + + console.log('') + console.log('#########################################') + console.log('# #') + console.log('# Refreshing contributions every minute #') + console.log('# #') + console.log('#########################################') + console.log('') + const users = await fetchUsers(); + + const usernames = users?.map((user: any) => user.username) || []; + + console.log("usernames:", usernames); + + for (let i = 0; i < usernames.length; i++) { + const contributionData = await fetchContributionData(usernames[i]); + if(!contributionData) continue; + // Check if the username already exists in the database + const { data: existingUser, error: fetchError } = await supabase + .from('contributions') + .select('*') + .eq('username', contributionData.username) + + console.log("existingUser:", existingUser, "fetchError:", fetchError); + + + if (fetchError) { + console.error("Fetch error:", fetchError); + continue; // Skip to the next username if there's an error + } + + if (existingUser.length) { + console.log("Update existing user: ", contributionData.username); + await supabase + .from('contributions') + .update(contributionData) + .eq('username', contributionData.username); + } else { + + console.log("Insert new user: ", contributionData.username); + contributorsData.push(contributionData); + } + } + + console.log("contributorsData:", contributorsData); + + // Upsert only new contributors + const updatedContributions = await supabase.from('contributions').upsert(contributorsData); + if (updatedContributions.error) + console.error("updatedContributions error:", updatedContributions.error) + // }); + return Response.json(contributorsData); +} + +const fetchUsers = async () => { + + let { data: Users, error } = await supabase + .from('Users') + .select('*') + + // console.log("Users:", Users) + + if (error) { console.log('error', error); return [] } + return Users; +} + +import { NextResponse } from "next/server"; + +import cron from 'node-cron'; + +export async function POST(req: any, res: any) { + + try { + + + + await fetch('/api/refreshDB', { method: "GET" }); + + + + return NextResponse.json({ data: 'Success', status: 200 }); + + } catch (error) { + console.log(error) + return NextResponse.json({ error: error }, { status: 500 }) + } + +} \ No newline at end of file diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx new file mode 100644 index 0000000..3fc9d54 --- /dev/null +++ b/app/components/Footer.tsx @@ -0,0 +1,18 @@ +import Link from "next/link"; + +export default function Footer() { + return ( + + + ) +} \ No newline at end of file diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx new file mode 100644 index 0000000..e1b8839 --- /dev/null +++ b/app/components/Hero.tsx @@ -0,0 +1,32 @@ +'use client' + +import Link from "next/link" + + +export default function Hero() { + return ( + <> +
+
+ + 100xLeaderboard + + +
+
+
+
+
+

+ 100xLeaderboard +

+

+ The Ultimate Showcase of Top Contributors

+
+
+
+
+
+ + ) +} diff --git a/app/components/Home.tsx b/app/components/Home.tsx new file mode 100644 index 0000000..c9901f1 --- /dev/null +++ b/app/components/Home.tsx @@ -0,0 +1,85 @@ +'use client' +import { Page, Box } from "grommet" +import { getSession, signIn, signOut, useSession } from "next-auth/react"; + +import { Component } from "./component"; +import { useState, useEffect } from "react"; +import Footer from "./Footer"; +import Hero from "./Hero"; + +import { Button } from "./ui/button"; +import { FaGithub } from "react-icons/fa"; + + +export default function Home() { + + const { data: session } = useSession(); + const [contributorData, setContributorData] = useState([]); + + useEffect(() => { + const fetchData = async () => { + const data = await fetch('/api/contributors?', { headers: { 'Cache-Control': 'no-cache' } }).then((res) => res.json()); + if (data.error) { setContributorData([]) } + else { + setContributorData(data); + } + }; + fetchData(); + + // const intervalId = setInterval(fetchData, 10000); // load every 10 seconds + + // return () => clearInterval(intervalId); // Cleanup on unmount + }, []); + + useEffect(() => { + if (session) { + //@ts-ignore + const access_token = session.accessToken || "test"; + + fetch('/api/contributors', {method:"POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ data: { access_token: access_token } })}).then((res) => res.json()); + } + } + , []) + + + return ( + <> + + + + + + {!session && +
+ +
+ + } + + {/* {session && */} + {/*
+ +
*/} + + + {/* {session ? : } */} +