diff --git a/package.json b/package.json
index 0ec387c1..c00cbd20 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
"@udecode/cn": "^44.0.1",
"flowbite": "^2.3.0",
"heroicons": "^2.1.1",
+ "jszip": "^3.10.1",
"lucide-react": "^0.469.0",
"micromodal": "^0.4.10",
"react": "^18.2.0",
diff --git a/src/components/dashboard_projects.jsx b/src/components/dashboard_projects.jsx
new file mode 100644
index 00000000..a215b8af
--- /dev/null
+++ b/src/components/dashboard_projects.jsx
@@ -0,0 +1,456 @@
+import { useEffect, useState } from "react";
+import {
+ Check,
+ Search,
+ ChevronLeft,
+ ChevronRight,
+ Download,
+ Loader,
+} from "lucide-react";
+import Dropdown from "~/components/dropdown.jsx";
+import { cn } from "@udecode/cn";
+import Badge from "~/components/badge.jsx";
+import InformationModal from "~/components/informationModal.jsx";
+import JSZip from "jszip";
+
+const options = [
+ { value: "all", label: "Filter Themes" },
+ { value: "mcsonae", label: "McSonae" },
+ { value: "uphold", label: "Uphold" },
+ { value: "singlestore", label: "SingleStore" },
+];
+
+export default function Dashboard() {
+ const [loading, setLoading] = useState(true);
+ const [loadingCommits, setLoadingCommits] = useState(false);
+ const [resultCommits, setResultCommits] = useState("");
+ const [projects, setProjects] = useState([]);
+ const [selectedOption, setSelectedOption] = useState("all");
+ const [filteredProjects, setFilteredProjects] = useState([]);
+ const [search, setSearch] = useState("");
+ const [currentPage, setCurrentPage] = useState(1);
+ const [dowloaingCvs, setDownloadingCvs] = useState(false);
+ const itemsPerPage = 5;
+ const totalPages = Math.ceil(filteredProjects.length / itemsPerPage);
+ const startIndex = (currentPage - 1) * itemsPerPage;
+ const endIndex = startIndex + itemsPerPage;
+ const currentProjects = filteredProjects.slice(startIndex, endIndex);
+
+ // Navigation functions
+ const goToNextPage = () => {
+ setCurrentPage((current) => Math.min(current + 1, totalPages));
+ };
+
+ const goToPreviousPage = () => {
+ setCurrentPage((current) => Math.max(current - 1, 1));
+ };
+
+ const goToPage = (pageNumber) => {
+ setCurrentPage(pageNumber);
+ };
+
+ useEffect(() => {
+ setCurrentPage(1);
+ }, [search, selectedOption]);
+
+ const getPageNumbers = () => {
+ const pageNumbers = [];
+ const maxVisiblePages = 3;
+
+ let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
+ const endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
+
+ // Adjust start page if we're near the end
+ if (endPage - startPage + 1 < maxVisiblePages) {
+ startPage = Math.max(1, endPage - maxVisiblePages + 1);
+ }
+
+ for (let i = startPage; i <= endPage; i++) {
+ pageNumbers.push(i);
+ }
+
+ return pageNumbers;
+ };
+
+ useEffect(() => {
+ async function fetchProjects() {
+ const response = await fetch("/api/projects/list", { method: "GET" });
+ const data = await response.json();
+ if (response.ok) {
+ setProjects(data);
+ setFilteredProjects(data);
+ }
+ }
+ fetchProjects();
+ }, []);
+
+ useEffect(() => {
+ async function checkAuth() {
+ const response = await fetch("/api/admin", {
+ method: "GET",
+ credentials: "include",
+ });
+ const data = await response.json();
+ if (!response.ok) {
+ window.location.href = "/";
+ } else {
+ setLoading(false);
+ }
+ }
+ checkAuth();
+ }, []);
+
+ useEffect(() => {
+ if (search) {
+ const searchResults = projects.filter(
+ (project) =>
+ project.name.toLowerCase().includes(search.toLowerCase()) ||
+ project.team_code.toLowerCase().includes(search.toLowerCase()),
+ );
+ setFilteredProjects(searchResults);
+ } else {
+ filterProjects(selectedOption);
+ }
+ }, [search]);
+
+ async function filterProjects(option) {
+ setSelectedOption(option);
+
+ if (option === "all") {
+ setFilteredProjects(projects);
+ } else if (option === "mcsonae") {
+ const mcsonaeProjects = projects.filter(
+ (team) => team.theme === "McSonae",
+ );
+ setFilteredProjects(mcsonaeProjects);
+ } else if (option === "uphold") {
+ const upholdProjects = projects.filter((team) => team.theme === "Uphold");
+ setFilteredProjects(upholdProjects);
+ } else if (option === "singlestore") {
+ const singlestoreProjects = projects.filter(
+ (team) => team.theme === "SingleStore",
+ );
+ setFilteredProjects(singlestoreProjects);
+ }
+ }
+
+ async function downloadCvs() {
+ setDownloadingCvs(true);
+
+ const queryParams = new URLSearchParams({
+ codes: filteredProjects.map((project) => project.team_code).join(","),
+ }).toString();
+
+ const participants_request = await fetch(
+ `/api/participants/list?${queryParams}`,
+ {
+ method: "GET",
+ },
+ );
+
+ const teams_request = await fetch("/api/teams/list", {
+ method: "GET",
+ });
+
+ const map_code_participants = await participants_request.json();
+ const list_teams = await teams_request.json();
+
+ const map_teams_participants = {};
+
+ list_teams.forEach((team) => {
+ const teamCode = team.code;
+ const teamName = team.name;
+
+ if (map_code_participants[teamCode]) {
+ // Map the team name to the corresponding participants
+ map_teams_participants[teamName] = map_code_participants[teamCode];
+ }
+ });
+
+ const map_teams_cvs = {};
+
+ await Promise.all(
+ Object.entries(map_teams_participants).map(
+ async ([teamName, participants]) => {
+ const queryParamsCvs = new URLSearchParams({
+ emails: participants.join(","),
+ }).toString();
+
+ const cvsrequest = await fetch(
+ `/api/cvs/download?${queryParamsCvs}`,
+ {
+ method: "GET",
+ },
+ );
+
+ if (!cvsrequest.ok) {
+ console.error(`Failed to download CVs for ${teamName}`);
+ return;
+ }
+
+ const blob = await cvsrequest.blob();
+ map_teams_cvs[teamName] = blob;
+ },
+ ),
+ );
+
+ if (Object.keys(map_teams_cvs).length === 0) {
+ console.error("No CVs were downloaded.");
+ return;
+ }
+
+ // Create ZIP file
+ const zip = new JSZip();
+ for (const [teamName, blob] of Object.entries(map_teams_cvs)) {
+ zip.file(`${teamName}.zip`, blob);
+ }
+
+ // Generate and download ZIP
+ const zipBlob = await zip.generateAsync({ type: "blob" });
+ const link = document.createElement("a");
+ link.href = URL.createObjectURL(zipBlob);
+ link.download = selectedOption + ".zip";
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ setDownloadingCvs(false);
+ }
+
+ async function CheckCommits(team_code) {
+ setLoadingCommits(true);
+ const response = await fetch(`/api/projects/commits?team=${team_code}`, {
+ method: "GET",
+ });
+ const data = await response.json();
+ setLoadingCommits(false);
+ if (response.ok) {
+ const result = data.message.valid
+ ? "Last commit is valid"
+ : "Last commit after the deadline";
+ setResultCommits(result);
+ } else {
+ setResultCommits("Error fetching commits");
+ }
+ }
+
+ if (loading) {
+ return (
+
+ Loading...
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TEAM CODE
+ |
+
+ NAME
+ |
+
+ LINK
+ |
+
+ SUBMITTED AT
+ |
+
+ THEME
+ |
+
+ DESCRIPTION
+ |
+
+ COMMITS
+ |
+
+
+
+ {filteredProjects.length > 0 ? (
+ currentProjects.map((project) => (
+
+
+ {project.team_code}
+ |
+
+ {project.name}
+ |
+
+ {project.link}
+ |
+
+ {new Date(project.created_at).toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ })}
+ |
+
+ {project.theme}
+ |
+
+ {project.description ? project.description : "None"}
+ |
+
+
+ |
+
+ ))
+ ) : (
+
+
+ No Projects found matching your search.
+ |
+
+ )}
+
+
+
+ {loadingCommits && (
+
setLoadingCommits(false)}
+ />
+ )}
+ {resultCommits && (
+ setResultCommits("")}
+ />
+ )}
+
+
+ Showing {startIndex + 1}-
+ {Math.min(endIndex, filteredProjects.length)} of{" "}
+ {filteredProjects.length} projects
+
+
+
+
+
+ {getPageNumbers().map((pageNumber) => (
+
+ ))}
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/forms/adminForm.jsx b/src/components/forms/adminForm.jsx
index 0ca7ef03..3e61d674 100644
--- a/src/components/forms/adminForm.jsx
+++ b/src/components/forms/adminForm.jsx
@@ -11,7 +11,7 @@ export default function AdminForm() {
credentials: "include",
});
if (response.ok) {
- window.location.href = "/admin/dashboard";
+ window.location.href = "/admin/teams";
}
}
checkAuth();
@@ -34,7 +34,7 @@ export default function AdminForm() {
if (!response.ok) {
setError(data.message.error);
} else {
- window.location.href = "/admin/dashboard"; // Redirect to dashboard
+ window.location.href = "/admin/teams"; // Redirect to dashboard
}
} catch (err) {
setError("An unexpected error occurred. Please try again.");
diff --git a/src/components/forms/selector.jsx b/src/components/forms/selector.jsx
index e578c199..6ffb69c4 100644
--- a/src/components/forms/selector.jsx
+++ b/src/components/forms/selector.jsx
@@ -16,7 +16,9 @@ export default function Selector({ param, title, options }) {
--Please choose an option--
{options.map((option) => (
-
+
))}
diff --git a/src/components/informationModal.jsx b/src/components/informationModal.jsx
index 25a16922..32fcd43d 100644
--- a/src/components/informationModal.jsx
+++ b/src/components/informationModal.jsx
@@ -14,7 +14,7 @@ export default function InformationModal({ title, content, closeModal }) {
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
- class="lucide lucide-circle-check-big text-center text-green-500 h-12 w-12"
+ className="lucide lucide-circle-check-big text-center text-green-500 h-12 w-12"
>
diff --git a/src/components/projectSubmission.jsx b/src/components/projectSubmission.jsx
index c460fe79..988b1b22 100644
--- a/src/components/projectSubmission.jsx
+++ b/src/components/projectSubmission.jsx
@@ -16,7 +16,6 @@ export default function ProjectDelivery() {
const [showInfoModal, setShowInfoModal] = useState(false);
async function submit(e) {
- console.log("submit");
e.preventDefault();
closeModal();
setLoadingState(true);
@@ -27,7 +26,7 @@ export default function ProjectDelivery() {
});
const data = await response.json();
- console.log(data);
+
if (!response.ok) {
setResponseErrors(data.message.errors);
setLoadingState(false);
@@ -85,7 +84,7 @@ export default function ProjectDelivery() {
title="Project Theme"
options={themes.map((theme) => ({
key: theme.company,
- value: `${theme.company}: ${theme.theme}`
+ value: `${theme.company}: ${theme.theme}`,
}))}
/>
@@ -117,14 +116,14 @@ export default function ProjectDelivery() {
title="Project submitted!"
content={
<>
- Your project has been successfully submitted!
- Thank you for participating in Hackathon Bugsbyte. Good luck!
+ Your project has been successfully submitted! Thank you for
+ participating in{" "}
+ Hackathon Bugsbyte. Good luck!
>
}
closeModal={goBack}
/>
)}
-
);
diff --git a/src/components/registerForm.jsx b/src/components/registerForm.jsx
index f4ef17e3..9d017d1d 100644
--- a/src/components/registerForm.jsx
+++ b/src/components/registerForm.jsx
@@ -24,7 +24,7 @@ export default function Form() {
closeConfirmationModal();
setLoadingState(true);
const formData = new FormData(e.target);
- const response = await fetch("/api/register", {
+ const response = await fetch("/api/participants/register", {
method: "POST",
body: formData,
});
@@ -44,7 +44,6 @@ export default function Form() {
setResponseErrors(responseJoinTeam.message.errors);
setLoadingState(false);
} else {
- console.log("opening modal");
openInformationModal();
}
}
diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx
index 66bcbd41..16bda584 100644
--- a/src/components/sidebar.tsx
+++ b/src/components/sidebar.tsx
@@ -3,16 +3,12 @@
import { useEffect, useRef, useState } from "react";
import { X } from "lucide-react";
-interface SidebarItem {
- name: string;
- url: string;
-}
-
-interface SidebarProps {
- items?: SidebarItem[];
-}
+const items = [
+ { name: "Teams", url: "/admin/teams" },
+ { name: "Projects", url: "/admin/projects" },
+];
-export default function Sidebar({ items = [] }: SidebarProps) {
+export default function Sidebar() {
const [isOpen, setIsOpen] = useState(false);
const sidebarRef = useRef(null);
diff --git a/src/data/themes.json b/src/data/themes.json
index febb2302..0b74222a 100644
--- a/src/data/themes.json
+++ b/src/data/themes.json
@@ -1,14 +1,14 @@
[
- {
- "company": "McSonae",
- "theme": "Price optimization tool"
- },
- {
- "company": "Uphold",
- "theme": "Crypto basket price predictor"
- },
- {
- "company": "SingleStore",
- "theme": "Real-Time GenAI: Build Smarter, Faster AI Apps"
- }
-]
\ No newline at end of file
+ {
+ "company": "McSonae",
+ "theme": "Price optimization tool"
+ },
+ {
+ "company": "Uphold",
+ "theme": "Crypto basket price predictor"
+ },
+ {
+ "company": "SingleStore",
+ "theme": "Real-Time GenAI: Build Smarter, Faster AI Apps"
+ }
+]
diff --git a/src/hiddenpages/api/projects/create.ts b/src/hiddenpages/api/projects/create.ts
index 08249b93..165259d5 100644
--- a/src/hiddenpages/api/projects/create.ts
+++ b/src/hiddenpages/api/projects/create.ts
@@ -67,22 +67,23 @@ const validateForms = async (formData: FormData, errors: String[]) => {
if (!team_code || !link) {
errors.push("All fields are required.");
- return false
+ return false;
}
- const valid = (await validateTeamCode(team_code, errors) && await validateLink(link, errors))
+ const valid =
+ (await validateTeamCode(team_code, errors)) &&
+ (await validateLink(link, errors));
return valid;
};
-
const validateTeamCode = async (team_code: string, errors: String[]) => {
let valid = true;
let { data: projects, error } = await supabase
- .from("projects")
- .select("*")
- .eq("team_code", team_code);
+ .from("projects")
+ .select("*")
+ .eq("team_code", team_code);
if (error) {
errors.push(
@@ -95,7 +96,7 @@ const validateTeamCode = async (team_code: string, errors: String[]) => {
}
return valid;
-}
+};
const validateLink = async (link: string, errors: String[]) => {
const links = link.split(" ");
@@ -111,10 +112,11 @@ const validateLink = async (link: string, errors: String[]) => {
}
return true;
-}
+};
const validateGithubLink = async (link: string, errors: String[]) => {
- const githubLinkRegex = /^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/;
+ const githubLinkRegex =
+ /^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/;
const match = link.match(githubLinkRegex);
if (!match) {
@@ -138,4 +140,4 @@ const validateGithubLink = async (link: string, errors: String[]) => {
}
return true;
-}
\ No newline at end of file
+};
diff --git a/src/pages/admin/projects.astro b/src/pages/admin/projects.astro
new file mode 100644
index 00000000..dc3398c4
--- /dev/null
+++ b/src/pages/admin/projects.astro
@@ -0,0 +1,12 @@
+---
+import BaseLayout from "~/layouts/baseLayout.astro";
+import DashboardComponent from "~/components/dashboard_projects.jsx";
+import Sidebar from "~/components/sidebar.tsx";
+---
+
+
+
+
+
+
+
diff --git a/src/pages/admin/dashboard.astro b/src/pages/admin/teams.astro
similarity index 70%
rename from src/pages/admin/dashboard.astro
rename to src/pages/admin/teams.astro
index c8922f20..ab1df1c2 100644
--- a/src/pages/admin/dashboard.astro
+++ b/src/pages/admin/teams.astro
@@ -2,12 +2,10 @@
import BaseLayout from "~/layouts/baseLayout.astro";
import DashboardComponent from "~/components/dashboard.jsx";
import Sidebar from "~/components/sidebar.tsx";
-
-const navigationItems = [{ name: "Teams", url: "/admin/dashboard" }];
---
-
+
diff --git a/src/pages/api/cvs/download.ts b/src/pages/api/cvs/download.ts
new file mode 100644
index 00000000..e0be85d6
--- /dev/null
+++ b/src/pages/api/cvs/download.ts
@@ -0,0 +1,82 @@
+import type { APIRoute } from "astro";
+import { createClient } from "@supabase/supabase-js";
+import JSZip from "jszip";
+
+export const prerender = false;
+
+const supabase = createClient(
+ import.meta.env.SUPABASE_URL,
+ import.meta.env.SUPABASE_ANON_KEY,
+);
+
+const AUTH_COOKIE_NAME = "authToken";
+const AUTH_SECRET = import.meta.env.AUTH_SECRET;
+
+export const GET: APIRoute = async ({ request, cookies }) => {
+ const authToken = cookies.get(AUTH_COOKIE_NAME);
+
+ if (!authToken || authToken.value !== AUTH_SECRET) {
+ return new Response(
+ JSON.stringify({ message: { error: "Unauthorized" } }),
+ { status: 401 },
+ );
+ }
+
+ const url = new URL(request.url);
+ const emailsParam = url.searchParams.get("emails");
+ const emails = emailsParam ? emailsParam.split(",") : null;
+
+ if (emails && Array.isArray(emails)) {
+ const zip = new JSZip();
+
+ for (const email of emails) {
+ const name = email.split("@")[0];
+ const path = `cv/${email}/${name}.pdf`;
+ const { data: cv, error } = await supabase.storage
+ .from("files")
+ .download(path);
+
+ if (error) {
+ const path_PDF = `cv/${email}/${name}.PDF`;
+ const { data: cv_PDF, error: error_PDF } = await supabase.storage
+ .from("files")
+ .download(path_PDF);
+
+ if (error_PDF) {
+ console.error("Error downloading CV:", name);
+ return new Response(
+ JSON.stringify({ message: { error: "CV not found" } }),
+ { status: 404 },
+ );
+ }
+
+ const arrayBuffer = await cv_PDF.arrayBuffer();
+ zip.file(`${name}.pdf`, arrayBuffer);
+ continue;
+ }
+
+ const arrayBuffer = await cv.arrayBuffer();
+ zip.file(`${name}.pdf`, arrayBuffer);
+ }
+
+ if (Object.keys(zip.files).length === 0) {
+ return new Response(JSON.stringify({ error: "No CVs found" }), {
+ status: 404,
+ });
+ }
+
+ const zipBlob = await zip.generateAsync({ type: "blob" });
+
+ return new Response(zipBlob, {
+ headers: {
+ "Content-Disposition": "attachment; filename=cvs.zip",
+ "Content-Type": "application/zip",
+ },
+ });
+ }
+
+ return new Response(
+ JSON.stringify({ message: { error: "Invalid request" } }),
+ { status: 400 },
+ );
+};
diff --git a/src/pages/api/participants/list.ts b/src/pages/api/participants/list.ts
new file mode 100644
index 00000000..65c8f1f6
--- /dev/null
+++ b/src/pages/api/participants/list.ts
@@ -0,0 +1,67 @@
+import type { APIRoute } from "astro";
+import { createClient } from "@supabase/supabase-js";
+
+export const prerender = false;
+
+const supabase = createClient(
+ import.meta.env.SUPABASE_URL,
+ import.meta.env.SUPABASE_ANON_KEY,
+);
+
+const AUTH_COOKIE_NAME = "authToken";
+const AUTH_SECRET = import.meta.env.AUTH_SECRET;
+
+export const GET: APIRoute = async ({ request, cookies }) => {
+ const authToken = cookies.get(AUTH_COOKIE_NAME);
+
+ if (!authToken || authToken.value !== AUTH_SECRET) {
+ return new Response(
+ JSON.stringify({ message: { error: "Unauthorized" } }),
+ { status: 401 },
+ );
+ }
+
+ const url = new URL(request.url);
+ const codesParam = url.searchParams.get("codes");
+ const codes = codesParam ? codesParam.split(",") : null;
+
+ if (codes && Array.isArray(codes)) {
+ const { data: participants, error } = await supabase
+ .from("participants")
+ .select("*")
+ .in("team_code", codes);
+
+ if (error) {
+ return new Response(
+ JSON.stringify({ message: { error: error.message } }),
+ {
+ status: 500,
+ },
+ );
+ }
+
+ const result = codes.reduce>(
+ (acc, code) => {
+ acc[code] = participants
+ .filter((participant) => participant.team_code === code)
+ .map((participant) => participant.email);
+ return acc;
+ },
+ {},
+ );
+
+ return new Response(JSON.stringify(result), { status: 200 });
+ }
+
+ const { data: teams, error } = await supabase
+ .from("participants")
+ .select("*");
+
+ if (error) {
+ return new Response(JSON.stringify({ message: { error: error.message } }), {
+ status: 500,
+ });
+ }
+
+ return new Response(JSON.stringify(teams), { status: 200 });
+};
diff --git a/src/pages/api/register.ts b/src/pages/api/participants/register.ts
similarity index 100%
rename from src/pages/api/register.ts
rename to src/pages/api/participants/register.ts
diff --git a/src/pages/api/projects/commits.ts b/src/pages/api/projects/commits.ts
new file mode 100644
index 00000000..72c0626c
--- /dev/null
+++ b/src/pages/api/projects/commits.ts
@@ -0,0 +1,112 @@
+import type { APIRoute } from "astro";
+import { createClient } from "@supabase/supabase-js";
+
+export const prerender = false;
+
+const supabase = createClient(
+ import.meta.env.SUPABASE_URL,
+ import.meta.env.SUPABASE_ANON_KEY,
+);
+
+const AUTH_COOKIE_NAME = "authToken";
+const AUTH_SECRET = import.meta.env.AUTH_SECRET;
+
+const apiGithub = "https://api.github.com/repos/";
+const lastCommitDate = new Date("2025-03-30T13:30:00Z");
+
+export const GET: APIRoute = async ({ request, cookies }) => {
+ const authToken = cookies.get(AUTH_COOKIE_NAME);
+
+ if (!authToken || authToken.value !== AUTH_SECRET) {
+ return new Response(
+ JSON.stringify({ message: { error: "Unauthorized" } }),
+ { status: 401 },
+ );
+ }
+
+ const url = new URL(request.url);
+ const teamParam = url.searchParams.get("team");
+
+ if (!teamParam) {
+ return new Response(
+ JSON.stringify({ message: { error: "Team not found" } }),
+ { status: 404 },
+ );
+ }
+
+ const { data: links, error } = await supabase
+ .from("projects")
+ .select("link")
+ .eq("team_code", teamParam)
+ .single();
+
+ if (error) {
+ return new Response(
+ JSON.stringify({ message: { error: "Commits not found" } }),
+ { status: 404 },
+ );
+ }
+
+ const valid = await checkCommits(links.link);
+
+ if (!valid) {
+ return new Response(
+ JSON.stringify({ message: { valid: false, error: "Invalid commits" } }),
+ { status: 200 },
+ );
+ }
+ return new Response(
+ JSON.stringify({
+ message: { valid: true, error: null },
+ status: 200,
+ }),
+ { status: 200 },
+ );
+};
+
+const checkCommits = async (link: string) => {
+ const links = typeof link === "string" ? link.split(" ") : [];
+
+ for (let i = 0; i < links.length; i++) {
+ const link = links[i].trim();
+ if (link.length > 0) {
+ const valid = await validateCommit(link);
+ if (!valid) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+};
+
+const validateCommit = async (link: string) => {
+ const githubLinkRegex =
+ /^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/;
+
+ const match = link.match(githubLinkRegex);
+ if (!match) {
+ console.log("Invalid link format");
+ return false;
+ }
+
+ const [, username, repositoryName] = match;
+
+ const response = await fetch(
+ apiGithub + `${username}/${repositoryName}/commits`,
+ );
+ if (!response.ok) {
+ console.log("Error fetching commits");
+ return false;
+ }
+
+ const data = await response.json();
+ const lastCommit = new Date(data[0].commit.author.date);
+
+ if (lastCommit >= lastCommitDate) {
+ console.log("Last commit is too recent");
+ return false;
+ }
+
+ return true;
+};
diff --git a/src/pages/api/projects/list.ts b/src/pages/api/projects/list.ts
new file mode 100644
index 00000000..7352f320
--- /dev/null
+++ b/src/pages/api/projects/list.ts
@@ -0,0 +1,33 @@
+import type { APIRoute } from "astro";
+import { createClient } from "@supabase/supabase-js";
+
+export const prerender = false;
+
+const supabase = createClient(
+ import.meta.env.SUPABASE_URL,
+ import.meta.env.SUPABASE_ANON_KEY,
+);
+
+const AUTH_COOKIE_NAME = "authToken";
+const AUTH_SECRET = import.meta.env.AUTH_SECRET;
+
+export const GET: APIRoute = async ({ request, cookies }) => {
+ const authToken = cookies.get(AUTH_COOKIE_NAME);
+
+ if (!authToken || authToken.value !== AUTH_SECRET) {
+ return new Response(
+ JSON.stringify({ message: { error: "Unauthorized" } }),
+ { status: 401 },
+ );
+ }
+
+ const { data: teams, error } = await supabase.from("projects").select("*");
+
+ if (error) {
+ return new Response(JSON.stringify({ message: { error: error.message } }), {
+ status: 500,
+ });
+ }
+
+ return new Response(JSON.stringify(teams), { status: 200 });
+};