diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..584c233
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,24 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Next.js: debug server",
+ "runtimeExecutable": "npm",
+ "runtimeArgs": ["run", "dev"],
+ "port": 9229,
+ "console": "integratedTerminal",
+ "env": {
+ "NODE_OPTIONS": "--inspect"
+ }
+ },
+ {
+ "type": "chrome",
+ "request": "launch",
+ "name": "Chrome: debug frontend",
+ "url": "http://localhost:5000",
+ "webRoot": "${workspaceFolder}"
+ }
+ ]
+}
diff --git a/package.json b/package.json
index 59daf78..00fea50 100644
--- a/package.json
+++ b/package.json
@@ -21,17 +21,21 @@
"@mui/icons-material": "^7.0.2",
"@mui/joy": "^5.0.0-beta.52",
"@mui/material": "^7.0.2",
+ "@octokit/auth-app": "^7.2.1",
"@octokit/auth-oauth-app": "^8.1.4",
"@octokit/rest": "^21.1.1",
"bytez.js": "^1.1.2",
"dotenv": "^16.5.0",
"firebase": "^11.6.1",
"firebase-admin": "^13.3.0",
+ "highlight.js": "^11.11.1",
"langchain": "^0.3.24",
"luxon": "^3.6.1",
"next": "^15.3.1",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "react-markdown": "^10.1.0",
+ "rehype-highlight": "^7.0.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3.0.0",
diff --git a/src/app/api/webhook/github/route.js b/src/app/api/webhook/github/route.js
new file mode 100644
index 0000000..df7bdeb
--- /dev/null
+++ b/src/app/api/webhook/github/route.js
@@ -0,0 +1,93 @@
+import { Octokit } from "@octokit/rest";
+import { createAppAuth } from "@octokit/auth-app";
+import {
+ firestore,
+ collection,
+ getDocs,
+ query,
+ where,
+ update,
+ arrayUnion,
+} from "@/service/firebase/firestore";
+
+export async function POST(req) {
+ try {
+ const body = await req.json();
+
+ if (body.action === "created" || body.action === "deleted") {
+ return new Response("success", {
+ status: 200,
+ });
+ }
+
+ const {
+ action,
+ installation,
+ issue,
+ repository: { full_name },
+ } = body;
+
+ if (action === "opened" || action === "deleted") {
+ const usersRef = collection(firestore, "users");
+
+ const querySnapshot = await getDocs(
+ query(usersRef, where("installation_id", "==", installation.id))
+ );
+ const users = [];
+ querySnapshot.forEach((doc) => {
+ users.push({ id: doc.id, ...doc.data() });
+ });
+
+ const [user] = users;
+
+ const { html_url: url, title, body, number: issueNumber } = issue;
+
+ if (action === "opened") {
+ // INSERT MODEL THAT FINDS PROBLEMS AND TAGS AN APPROPRIATE MAINTAINER HERE
+
+ const response = "We're on it, @inf3rnus check this out 👀";
+
+ // Add comment on GitHub
+ const octokit = new Octokit({
+ authStrategy: createAppAuth,
+ auth: {
+ appId: process.env.GITHUB_APP_ID,
+ privateKey: process.env.GITHUB_APP_PRIVATE_KEY.replace(
+ /\\n/g,
+ "\n"
+ ),
+ installationId: installation.id,
+ },
+ });
+
+ await octokit.issues.createComment({
+ owner: full_name.split("/")[0],
+ repo: full_name.split("/")[1],
+ issue_number: issueNumber,
+ body: response,
+ });
+
+ const updatedIssue = { url, title, body, response };
+
+ await update(`users/${user.id}`, {
+ issues: arrayUnion(updatedIssue),
+ });
+ } else {
+ const newIssues = user.issues.filter((issue) => issue.url !== url);
+
+ await update(`users/${user.id}`, {
+ issues: newIssues,
+ });
+ }
+ }
+
+ return new Response("success", {
+ status: 200,
+ });
+ } catch (error) {
+ console.error(error);
+ return new Response("failed", {
+ status: 500,
+ });
+ }
+}
diff --git a/src/app/api/webhook/github_install_app/route.js b/src/app/api/webhook/github_install_app/route.js
new file mode 100644
index 0000000..991cbef
--- /dev/null
+++ b/src/app/api/webhook/github_install_app/route.js
@@ -0,0 +1,20 @@
+import { update } from "@/service/firebase/firestore";
+
+// Redirect users to GitHub's authorization page to begin the app installation flow
+export async function GET(req) {
+ const url = new URL(req.url);
+ const uid = url.searchParams.get("uid");
+
+ const githubAuthUrl = new URL(
+ `https://github.com/apps/${process.env.GITHUB_APP_NAME}/installations/new`
+ );
+
+ const githubAppInstallState = crypto.randomUUID();
+
+ await update(`users/${uid}`, { githubAppInstallState });
+
+ githubAuthUrl.searchParams.append("state", githubAppInstallState);
+
+ // Redirect to GitHub
+ return Response.redirect(githubAuthUrl.toString());
+}
diff --git a/src/app/api/webhook/github_install_app_callback/route.js b/src/app/api/webhook/github_install_app_callback/route.js
new file mode 100644
index 0000000..9562571
--- /dev/null
+++ b/src/app/api/webhook/github_install_app_callback/route.js
@@ -0,0 +1,57 @@
+import { Octokit } from "@octokit/rest";
+import { createAppAuth } from "@octokit/auth-app";
+import {
+ firestore,
+ collection,
+ getDocs,
+ query,
+ where,
+ update,
+ deleteField,
+} from "@/service/firebase/firestore";
+
+export async function GET(req) {
+ try {
+ const url = new URL(req.url);
+ const installation_id = Number(url.searchParams.get("installation_id"));
+ const state = url.searchParams.get("state");
+
+ const usersRef = collection(firestore, "users");
+
+ const querySnapshot = await getDocs(
+ query(usersRef, where("githubAppInstallState", "==", state))
+ );
+ const users = [];
+ querySnapshot.forEach((doc) => {
+ users.push({ id: doc.id, ...doc.data() });
+ });
+
+ const [user] = users;
+
+ const octokit = new Octokit({
+ authStrategy: createAppAuth,
+ auth: {
+ appId: process.env.GITHUB_APP_ID,
+ privateKey: process.env.GITHUB_APP_PRIVATE_KEY.replace(/\\n/g, "\n"),
+ installationId: installation_id,
+ },
+ });
+
+ const { data } = await octokit.request("GET /installation/repositories");
+
+ const [repoData] = data.repositories;
+
+ await update(`users/${user.id}`, {
+ installation_id,
+ repoData,
+ repo: repoData.html_url,
+ githubAppInstallState: deleteField(),
+ });
+
+ return Response.redirect(`${process.env.NEXT_PUBLIC_APP_URL}/dashboard`);
+ } catch (error) {
+ console.error("Error fetching repositories:", error);
+
+ return new Response("Error fetching repositories", { status: 500 });
+ }
+}
diff --git a/src/app/dashboard/page.js b/src/app/dashboard/page.js
index 9fac88d..1dd0580 100644
--- a/src/app/dashboard/page.js
+++ b/src/app/dashboard/page.js
@@ -1,8 +1,17 @@
"use client";
import { useEffect, useState } from "react";
-import { useRouter } from "next/navigation";
-import { Typography, Box, Input, Button, Stack } from "@mui/joy";
-import { getAuth } from "firebase/auth";
+import {
+ Typography,
+ Box,
+ Button,
+ Card,
+ CardContent,
+ CardActions,
+ Stack,
+ Input,
+} from "@mui/joy";
+import { Check as CheckIcon } from "@mui/icons-material";
+import { getAuth, onAuthStateChanged } from "firebase/auth";
import Sidebar from "@/component/Sidebar";
import { listen, set } from "@/service/firebase/firestore";
@@ -12,24 +21,23 @@ export default function Dashboard() {
const [user, setUser] = useState();
const [repo, setRepo] = useState("");
const [saving, setSaving] = useState(false);
- const router = useRouter();
useEffect(() => {
- const { currentUser } = getAuth();
-
- if (currentUser === null) {
- return router.replace("/");
- }
-
- setSession(currentUser);
+ return onAuthStateChanged(getAuth(), (session) => {
+ setSession(session);
+ });
+ }, []);
- return listen(`users/${currentUser.uid}`, (doc) => {
- const userData = doc.data();
+ useEffect(() => {
+ if (session) {
+ return listen(`users/${session.uid}`, (doc) => {
+ const userData = doc.data();
- setUser(userData);
- setRepo(userData?.repo);
- });
- }, [router]);
+ setUser(userData);
+ setRepo(userData?.repo);
+ });
+ }
+ }, [session]);
return session ? (