From 884a9faf26f589e535ae1db32b56b51b657e1620 Mon Sep 17 00:00:00 2001 From: FilipeR13 Date: Thu, 27 Mar 2025 15:59:31 +0000 Subject: [PATCH 01/15] feat: improved submission projects --- src/components/header.astro | 8 +++ src/pages/api/projects/create.ts | 56 ++++++++++++++++--- src/{hidden_pages => pages}/projects.astro | 0 .../20250327150742_modify_projects_table.sql | 11 ++++ test.py | 13 +++++ 5 files changed, 81 insertions(+), 7 deletions(-) rename src/{hidden_pages => pages}/projects.astro (100%) create mode 100644 supabase/migrations/20250327150742_modify_projects_table.sql create mode 100644 test.py diff --git a/src/components/header.astro b/src/components/header.astro index 0c53beb5..77766729 100644 --- a/src/components/header.astro +++ b/src/components/header.astro @@ -44,6 +44,14 @@ const headerClass = sticky Register +
  • + + Upload Project + +
  • +
    + +
    + + +
    + + + + + + + + + + + + + {filteredProjects.length > 0 ? ( + currentProjects.map((project) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
    + TEAM CODE + + NAME + + LINK + + SUBMITTED AT + + THEME + + DESCRIPTION +
    + {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. +
    +
    +
    +
    + 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/projectSubmission.jsx b/src/components/projectSubmission.jsx index c460fe79..7cb3dba0 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); diff --git a/src/components/registerForm.jsx b/src/components/registerForm.jsx index c811686d..06ca2435 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..72679962 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/pages/admin/projects.astro b/src/pages/admin/projects.astro new file mode 100644 index 00000000..d9a33c76 --- /dev/null +++ b/src/pages/admin/projects.astro @@ -0,0 +1,13 @@ +--- +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..d0e27236 100644 --- a/src/pages/admin/dashboard.astro +++ b/src/pages/admin/teams.astro @@ -3,11 +3,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..e733c6f8 --- /dev/null +++ b/src/pages/api/cvs/download.ts @@ -0,0 +1,46 @@ +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 emailsParam = url.searchParams.get("emails"); + const emails = emailsParam ? emailsParam.split(",") : null; + + if (emails && Array.isArray(emails)) { + const { data: cvs, error } = await supabase + .storage + .from("files") + .list("cv") + + console.log("CVS", cvs); + } + + 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/participants/list.ts b/src/pages/api/participants/list.ts new file mode 100644 index 00000000..44f5aeab --- /dev/null +++ b/src/pages/api/participants/list.ts @@ -0,0 +1,59 @@ +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/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 }); +}; From b32d37baaa1413678e6d4082e02c53d3e4afa4cb Mon Sep 17 00:00:00 2001 From: FilipeR13 Date: Sun, 30 Mar 2025 12:39:31 +0100 Subject: [PATCH 06/15] k --- package.json | 1 + src/components/dashboard_projects.jsx | 13 ++----- src/pages/api/cvs/download.ts | 50 +++++++++++++++++++++------ 3 files changed, 43 insertions(+), 21 deletions(-) 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 index 33355644..9bc2b235 100644 --- a/src/components/dashboard_projects.jsx +++ b/src/components/dashboard_projects.jsx @@ -122,7 +122,6 @@ export default function Dashboard() { codes: projects.map((project) => project.team_code).join(","), }).toString(); - // Fetch the participants and teams data const participants_request = await fetch(`/api/participants/list?${queryParams}`, { method: "GET", }); @@ -131,27 +130,21 @@ export default function Dashboard() { method: "GET" }); - // Parse the JSON responses const map_code_participants = await participants_request.json(); const list_teams = await teams_request.json(); - // Create a map from team names to participants const map_teams_participants = {}; list_teams.forEach((team) => { - const teamCode = team.code; // Assuming "code" is the key that maps team codes - const teamName = team.name; // Extract the team name + 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]; - } else { - // If no participants are found for the team code, assign an empty array - map_teams_participants[teamName] = []; } }); - // Example: return or process map_teams_participants as needed console.log(map_teams_participants); const map_teams_cvs = {} @@ -167,9 +160,9 @@ export default function Dashboard() { const cvs = await cvsrequest.json(); map_teams_cvs[teamName] = cvs; - console.log(cvs); }); + console.log(map_teams_cvs); } diff --git a/src/pages/api/cvs/download.ts b/src/pages/api/cvs/download.ts index e733c6f8..9601b959 100644 --- a/src/pages/api/cvs/download.ts +++ b/src/pages/api/cvs/download.ts @@ -1,6 +1,7 @@ import type { APIRoute } from "astro"; import { createClient } from "@supabase/supabase-js"; + export const prerender = false; const supabase = createClient( @@ -25,22 +26,49 @@ export const GET: APIRoute = async ({ request, cookies }) => { const emailsParam = url.searchParams.get("emails"); const emails = emailsParam ? emailsParam.split(",") : null; + const cvs: Blob[] = []; + if (emails && Array.isArray(emails)) { - const { data: cvs, error } = await supabase - .storage - .from("files") - .list("cv") + 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); - console.log("CVS", cvs); - } + if (error) { + return new Response( + JSON.stringify({ message: { error: "CV not found" } }), + { status: 404 }, + ); + } + cvs.push(cv); + } + if (cvs.length === 0) { + return new Response( + JSON.stringify({ message: { error: "No CVs found" } }), + { status: 404 }, + ); + } + const zip = new JSZip(); + cvs.forEach((cv, index) => { + zip.file(`cv_${index + 1}.pdf`, cv); + }); - const { data: teams, error } = await supabase.from("participants").select("*"); + const zipBlob = await zip.generateAsync({ type: "blob" }); - if (error) { - return new Response(JSON.stringify({ message: { error: error.message } }), { - status: 500, + return new Response(zipBlob, { + status: 200, + headers: { + "Content-Type": "application/zip", + "Content-Disposition": "attachment; filename=cvs.zip", + }, }); } - return new Response(JSON.stringify(teams), { status: 200 }); + return new Response( + JSON.stringify({ message: { error: "Invalid request" } }), + { status: 400 }, + ); }; From bbaabbae27da4045f38340d5af514ef8bd647b5b Mon Sep 17 00:00:00 2001 From: FilipeR13 Date: Sun, 30 Mar 2025 13:30:22 +0100 Subject: [PATCH 07/15] s --- src/components/dashboard_projects.jsx | 32 +++++++++++++++++++++++++-- src/pages/api/cvs/download.ts | 5 ++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/components/dashboard_projects.jsx b/src/components/dashboard_projects.jsx index 9bc2b235..cef0d7ea 100644 --- a/src/components/dashboard_projects.jsx +++ b/src/components/dashboard_projects.jsx @@ -1,8 +1,9 @@ import { useEffect, useState } from "react"; -import { Search, ChevronLeft, ChevronRight, Download } from "lucide-react"; +import { Check, Search, ChevronLeft, ChevronRight, Download } 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"; const options = [ { value: "all", label: "Filter Themes" }, @@ -13,6 +14,8 @@ const options = [ export default function Dashboard() { const [loading, setLoading] = useState(true); + const [loadingCommits, setLoadingCommits] = useState(false); + const [showModal, setShowModal] = useState(false); const [projects, setProjects] = useState([]); const [selectedOption, setSelectedOption] = useState("all"); const [filteredProjects, setFilteredProjects] = useState([]); @@ -258,6 +261,12 @@ export default function Dashboard() { > DESCRIPTION + + COMMITS + @@ -293,6 +302,18 @@ export default function Dashboard() { "None" )} + + + )) ) : ( @@ -305,6 +326,13 @@ export default function Dashboard() { + {showModal && ( + setShowModal(false)} + /> + )}
    Showing {startIndex + 1}- diff --git a/src/pages/api/cvs/download.ts b/src/pages/api/cvs/download.ts index 9601b959..4cac3d5b 100644 --- a/src/pages/api/cvs/download.ts +++ b/src/pages/api/cvs/download.ts @@ -51,7 +51,9 @@ export const GET: APIRoute = async ({ request, cookies }) => { { status: 404 }, ); } - const zip = new JSZip(); + + // TODO: Create a zip file with the CVs + /* const zip = new JSZip(); cvs.forEach((cv, index) => { zip.file(`cv_${index + 1}.pdf`, cv); }); @@ -65,6 +67,7 @@ export const GET: APIRoute = async ({ request, cookies }) => { "Content-Disposition": "attachment; filename=cvs.zip", }, }); + */ } return new Response( From e2bdddc0177c1e956ff58d909927f056a0f9263e Mon Sep 17 00:00:00 2001 From: FilipeR13 Date: Sun, 30 Mar 2025 15:19:33 +0100 Subject: [PATCH 08/15] feat: check deadline of commits --- src/components/dashboard_projects.jsx | 138 ++++++++++++++----------- src/components/forms/selector.jsx | 4 +- src/components/informationModal.jsx | 2 +- src/components/projectSubmission.jsx | 8 +- src/components/sidebar.tsx | 2 +- src/data/themes.json | 26 ++--- src/hiddenpages/api/projects/create.ts | 24 +++-- src/pages/admin/projects.astro | 1 - src/pages/admin/teams.astro | 1 - src/pages/api/cvs/download.ts | 8 +- src/pages/api/participants/list.ts | 38 ++++--- src/pages/api/projects/commits.ts | 96 +++++++++++------ 12 files changed, 198 insertions(+), 150 deletions(-) diff --git a/src/components/dashboard_projects.jsx b/src/components/dashboard_projects.jsx index 58202161..9ec94ced 100644 --- a/src/components/dashboard_projects.jsx +++ b/src/components/dashboard_projects.jsx @@ -1,5 +1,11 @@ import { useEffect, useState } from "react"; -import { Check, Search, ChevronLeft, ChevronRight, Download } from "lucide-react"; +import { + Check, + Search, + ChevronLeft, + ChevronRight, + Download, +} from "lucide-react"; import Dropdown from "~/components/dropdown.jsx"; import { cn } from "@udecode/cn"; import Badge from "~/components/badge.jsx"; @@ -93,9 +99,10 @@ export default function Dashboard() { useEffect(() => { if (search) { - const searchResults = projects.filter((project) => - project.name.toLowerCase().includes(search.toLowerCase()) || - project.team_code.toLowerCase().includes(search.toLowerCase()), + const searchResults = projects.filter( + (project) => + project.name.toLowerCase().includes(search.toLowerCase()) || + project.team_code.toLowerCase().includes(search.toLowerCase()), ); setFilteredProjects(searchResults); } else { @@ -109,28 +116,35 @@ export default function Dashboard() { if (option === "all") { setFilteredProjects(projects); } else if (option === "mmcsonae") { - const mcsonaeProjects = projects.filter((team) => team.theme === "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"); + const singlestoreProjects = projects.filter( + (team) => team.theme === "SingleStore", + ); setFilteredProjects(singlestoreProjects); } } async function downloadCvs() { const queryParams = new URLSearchParams({ - codes: projects.map((project) => project.team_code).join(","), + codes: projects.map((project) => project.team_code).join(","), }).toString(); - const participants_request = await fetch(`/api/participants/list?${queryParams}`, { + const participants_request = await fetch( + `/api/participants/list?${queryParams}`, + { method: "GET", - }); + }, + ); const teams_request = await fetch("/api/teams/list", { - method: "GET" + method: "GET", }); const map_code_participants = await participants_request.json(); @@ -139,31 +153,33 @@ export default function Dashboard() { const map_teams_participants = {}; list_teams.forEach((team) => { - const teamCode = team.code; - const teamName = team.name; + 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]; - } + if (map_code_participants[teamCode]) { + // Map the team name to the corresponding participants + map_teams_participants[teamName] = map_code_participants[teamCode]; + } }); console.log(map_teams_participants); - const map_teams_cvs = {} + const map_teams_cvs = {}; - Object.entries(map_teams_participants).forEach(async ([teamName, participants]) => { - const queryParamsCvs = new URLSearchParams({ - emails: participants.join(","), - }).toString(); + Object.entries(map_teams_participants).forEach( + async ([teamName, participants]) => { + const queryParamsCvs = new URLSearchParams({ + emails: participants.join(","), + }).toString(); - const cvsrequest = await fetch(`/api/cvs/download?${queryParamsCvs}`, { - method: "GET", - }); - const cvs = await cvsrequest.json(); - - map_teams_cvs[teamName] = cvs; - }); + const cvsrequest = await fetch(`/api/cvs/download?${queryParamsCvs}`, { + method: "GET", + }); + const cvs = await cvsrequest.json(); + + map_teams_cvs[teamName] = cvs; + }, + ); console.log(map_teams_cvs); } @@ -176,18 +192,16 @@ export default function Dashboard() { const data = await response.json(); setLoadingCommits(false); if (response.ok) { - const result = data.valid ? ( - "Last commit is valid" - ) : ( - "Last commit after the deadline" - ); + const result = data.message.valid + ? "Last commit is valid" + : "Last commit after the deadline"; setResultCommits(result); } else { setResultCommits("Error fetching commits"); + console.error("Error fetching commits:", data.message.error); } } - if (loading) { return (
    @@ -217,26 +231,27 @@ export default function Dashboard() { />
    - + >
    - + + CVs
    + +
    + +
    @@ -301,24 +316,19 @@ export default function Dashboard() { {project.name} - - {project.link} - + {project.link} - {new Date(project.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + {new Date(project.created_at).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} {project.theme} - {project.description ? ( - project.description - ) : ( - "None" - )} + {project.description ? project.description : "None"} 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 7cb3dba0..988b1b22 100644 --- a/src/components/projectSubmission.jsx +++ b/src/components/projectSubmission.jsx @@ -84,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}`, }))} /> @@ -116,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/sidebar.tsx b/src/components/sidebar.tsx index 72679962..16bda584 100644 --- a/src/components/sidebar.tsx +++ b/src/components/sidebar.tsx @@ -5,7 +5,7 @@ import { X } from "lucide-react"; const items = [ { name: "Teams", url: "/admin/teams" }, - { name: "Projects", url: "/admin/projects" } + { name: "Projects", url: "/admin/projects" }, ]; export default function Sidebar() { 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..c9c6f2ce 100644 --- a/src/hiddenpages/api/projects/create.ts +++ b/src/hiddenpages/api/projects/create.ts @@ -11,7 +11,7 @@ const supabase = createClient( const apiGithub = "https://api.github.com/repos/"; // TODO: Change this date to the contest start date -const beginContestDate = new Date("2025-03-28T18:00:00Z"); +const beginContestDate = new Date("2000-03-28T18:00:00Z"); export const POST: APIRoute = async ({ request }) => { const formData = await request.formData(); @@ -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 index d9a33c76..dc3398c4 100644 --- a/src/pages/admin/projects.astro +++ b/src/pages/admin/projects.astro @@ -2,7 +2,6 @@ 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/teams.astro b/src/pages/admin/teams.astro index d0e27236..ab1df1c2 100644 --- a/src/pages/admin/teams.astro +++ b/src/pages/admin/teams.astro @@ -2,7 +2,6 @@ import BaseLayout from "~/layouts/baseLayout.astro"; import DashboardComponent from "~/components/dashboard.jsx"; import Sidebar from "~/components/sidebar.tsx"; - --- diff --git a/src/pages/api/cvs/download.ts b/src/pages/api/cvs/download.ts index 4cac3d5b..0535eb00 100644 --- a/src/pages/api/cvs/download.ts +++ b/src/pages/api/cvs/download.ts @@ -1,7 +1,6 @@ import type { APIRoute } from "astro"; import { createClient } from "@supabase/supabase-js"; - export const prerender = false; const supabase = createClient( @@ -14,14 +13,14 @@ 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; @@ -32,8 +31,7 @@ export const GET: APIRoute = async ({ request, cookies }) => { for (const email of emails) { const name = email.split("@")[0]; const path = `cv/${email}/${name}.pdf`; - const { data: cv, error } = await supabase - .storage + const { data: cv, error } = await supabase.storage .from("files") .download(path); diff --git a/src/pages/api/participants/list.ts b/src/pages/api/participants/list.ts index 44f5aeab..65c8f1f6 100644 --- a/src/pages/api/participants/list.ts +++ b/src/pages/api/participants/list.ts @@ -13,14 +13,14 @@ 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; @@ -30,24 +30,32 @@ export const GET: APIRoute = async ({ request, cookies }) => { .from("participants") .select("*") .in("team_code", codes); - + if (error) { - return new Response(JSON.stringify({ message: { error: error.message } }), { - status: 500, - }); + 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; - }, {}); - + + 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("*"); + const { data: teams, error } = await supabase + .from("participants") + .select("*"); if (error) { return new Response(JSON.stringify({ message: { error: error.message } }), { diff --git a/src/pages/api/projects/commits.ts b/src/pages/api/projects/commits.ts index b498cc77..72c0626c 100644 --- a/src/pages/api/projects/commits.ts +++ b/src/pages/api/projects/commits.ts @@ -12,17 +12,18 @@ 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"); @@ -33,7 +34,7 @@ export const GET: APIRoute = async ({ request, cookies }) => { ); } - const { data: commits, error } = await supabase + const { data: links, error } = await supabase .from("projects") .select("link") .eq("team_code", teamParam) @@ -45,40 +46,67 @@ export const GET: APIRoute = async ({ request, cookies }) => { { status: 404 }, ); } - const urls = commits.link.split(","); - const valid = checkCommits(urls); + 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(" ") : []; -const checkCommits = (urls: string[]) => { - const valid = urls.every((url) => { - const urlParts = url.split("/"); - const repoName = urlParts[urlParts.length - 1]; - const commitUrl = `${apiGithub}${repoName}/commits?per_page=1`; - - getLastCommitDate(commitUrl); - }); - - if (valid) { - return new Response( - JSON.stringify({ message: { error: "Commits not found" } }), - { status: 404 }, - ); + 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; + } } -} -async function getLastCommitDate() { - - try { - const response = await axios.get(url, { - headers: { - Accept: "application/vnd.github+json", - }, - }); - - const lastCommitDate = response.data[0].commit.committer.date; - console.log(`The last commit date is: ${lastCommitDate}`); - } catch (error) { - console.error("Error fetching the last commit date:", error); } -} + + 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; +}; From dbcbd91de93981e5b58208da47874c4517b9ba41 Mon Sep 17 00:00:00 2001 From: FilipeR13 Date: Mon, 31 Mar 2025 17:48:10 +0100 Subject: [PATCH 09/15] finished logic to download cvs --- src/components/dashboard_projects.jsx | 58 +++++++++++++++++++-------- src/pages/api/cvs/download.ts | 31 ++++++-------- 2 files changed, 55 insertions(+), 34 deletions(-) diff --git a/src/components/dashboard_projects.jsx b/src/components/dashboard_projects.jsx index 9ec94ced..9a9479e6 100644 --- a/src/components/dashboard_projects.jsx +++ b/src/components/dashboard_projects.jsx @@ -10,6 +10,7 @@ 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" }, @@ -162,26 +163,52 @@ export default function Dashboard() { } }); - console.log(map_teams_participants); - const map_teams_cvs = {}; - Object.entries(map_teams_participants).forEach( - async ([teamName, participants]) => { - const queryParamsCvs = new URLSearchParams({ - emails: participants.join(","), - }).toString(); + 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; + }, + ), + ); - const cvsrequest = await fetch(`/api/cvs/download?${queryParamsCvs}`, { - method: "GET", - }); - const cvs = await cvsrequest.json(); + if (Object.keys(map_teams_cvs).length === 0) { + console.error("No CVs were downloaded."); + return; + } - map_teams_cvs[teamName] = cvs; - }, - ); + // Create ZIP file + const zip = new JSZip(); + for (const [teamName, blob] of Object.entries(map_teams_cvs)) { + zip.file(`${teamName}.zip`, blob); + } - console.log(map_teams_cvs); + // 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); } async function CheckCommits(team_code) { @@ -198,7 +225,6 @@ export default function Dashboard() { setResultCommits(result); } else { setResultCommits("Error fetching commits"); - console.error("Error fetching commits:", data.message.error); } } diff --git a/src/pages/api/cvs/download.ts b/src/pages/api/cvs/download.ts index 0535eb00..64706112 100644 --- a/src/pages/api/cvs/download.ts +++ b/src/pages/api/cvs/download.ts @@ -1,5 +1,6 @@ import type { APIRoute } from "astro"; import { createClient } from "@supabase/supabase-js"; +import JSZip from "jszip"; export const prerender = false; @@ -25,9 +26,9 @@ export const GET: APIRoute = async ({ request, cookies }) => { const emailsParam = url.searchParams.get("emails"); const emails = emailsParam ? emailsParam.split(",") : null; - const cvs: Blob[] = []; - 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`; @@ -41,31 +42,25 @@ export const GET: APIRoute = async ({ request, cookies }) => { { status: 404 }, ); } - cvs.push(cv); - } - if (cvs.length === 0) { - return new Response( - JSON.stringify({ message: { error: "No CVs found" } }), - { status: 404 }, - ); + + const arrayBuffer = await cv.arrayBuffer(); + zip.file(`${name}.pdf`, arrayBuffer); } - // TODO: Create a zip file with the CVs - /* const zip = new JSZip(); - cvs.forEach((cv, index) => { - zip.file(`cv_${index + 1}.pdf`, cv); - }); + 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, { - status: 200, headers: { - "Content-Type": "application/zip", - "Content-Disposition": "attachment; filename=cvs.zip", + "Content-Disposition": "attachment; filename=cvs.zip", + "Content-Type": "application/zip", }, }); - */ } return new Response( From 45cf3f0bb84f1a9f1b1ea0545e8a0a81956ecb0c Mon Sep 17 00:00:00 2001 From: FilipeR13 Date: Mon, 31 Mar 2025 18:01:29 +0100 Subject: [PATCH 10/15] fix: typo --- src/components/dashboard_projects.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard_projects.jsx b/src/components/dashboard_projects.jsx index 9a9479e6..67058584 100644 --- a/src/components/dashboard_projects.jsx +++ b/src/components/dashboard_projects.jsx @@ -116,7 +116,7 @@ export default function Dashboard() { if (option === "all") { setFilteredProjects(projects); - } else if (option === "mmcsonae") { + } else if (option === "mcsonae") { const mcsonaeProjects = projects.filter( (team) => team.theme === "McSonae", ); From abc63c2e5b4bf32f06c830b5ff79bf7450176ec2 Mon Sep 17 00:00:00 2001 From: FilipeR13 Date: Mon, 31 Mar 2025 18:10:58 +0100 Subject: [PATCH 11/15] fix: cvs to download --- src/components/dashboard_projects.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard_projects.jsx b/src/components/dashboard_projects.jsx index 67058584..3e3039cf 100644 --- a/src/components/dashboard_projects.jsx +++ b/src/components/dashboard_projects.jsx @@ -134,7 +134,7 @@ export default function Dashboard() { async function downloadCvs() { const queryParams = new URLSearchParams({ - codes: projects.map((project) => project.team_code).join(","), + codes: filteredProjects.map((project) => project.team_code).join(","), }).toString(); const participants_request = await fetch( From ed06220fa512a973f3933b6521b1b461ea1b3744 Mon Sep 17 00:00:00 2001 From: FilipeR13 Date: Mon, 31 Mar 2025 18:21:24 +0100 Subject: [PATCH 12/15] feat: loader --- src/components/dashboard_projects.jsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/dashboard_projects.jsx b/src/components/dashboard_projects.jsx index 3e3039cf..a215b8af 100644 --- a/src/components/dashboard_projects.jsx +++ b/src/components/dashboard_projects.jsx @@ -5,6 +5,7 @@ import { ChevronLeft, ChevronRight, Download, + Loader, } from "lucide-react"; import Dropdown from "~/components/dropdown.jsx"; import { cn } from "@udecode/cn"; @@ -28,6 +29,7 @@ export default function Dashboard() { 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; @@ -133,6 +135,8 @@ export default function Dashboard() { } async function downloadCvs() { + setDownloadingCvs(true); + const queryParams = new URLSearchParams({ codes: filteredProjects.map((project) => project.team_code).join(","), }).toString(); @@ -209,6 +213,7 @@ export default function Dashboard() { document.body.appendChild(link); link.click(); document.body.removeChild(link); + setDownloadingCvs(false); } async function CheckCommits(team_code) { @@ -267,7 +272,12 @@ export default function Dashboard() { onClick={downloadCvs} >
    - + {dowloaingCvs && ( + + )} + {!dowloaingCvs && ( + + )} CVs
    From 325098950ffb9dcbbbb7ec99183f9bb1c37b26e4 Mon Sep 17 00:00:00 2001 From: FilipeR13 Date: Mon, 31 Mar 2025 18:50:57 +0100 Subject: [PATCH 13/15] fix: accept .PDF files --- src/pages/api/cvs/download.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/pages/api/cvs/download.ts b/src/pages/api/cvs/download.ts index 64706112..e0be85d6 100644 --- a/src/pages/api/cvs/download.ts +++ b/src/pages/api/cvs/download.ts @@ -37,10 +37,22 @@ export const GET: APIRoute = async ({ request, cookies }) => { .download(path); if (error) { - return new Response( - JSON.stringify({ message: { error: "CV not found" } }), - { status: 404 }, - ); + 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(); From 8bb4a80cdd856d137d03e1e1eafbd14ed64de437 Mon Sep 17 00:00:00 2001 From: FilipeR13 Date: Mon, 31 Mar 2025 18:54:30 +0100 Subject: [PATCH 14/15] disabled project submission --- src/components/header.astro | 8 -------- src/pages/projects.astro | 10 ---------- 2 files changed, 18 deletions(-) delete mode 100644 src/pages/projects.astro diff --git a/src/components/header.astro b/src/components/header.astro index 7e925a97..37b92900 100644 --- a/src/components/header.astro +++ b/src/components/header.astro @@ -44,14 +44,6 @@ const headerClass = sticky Register -
  • - - Upload Project - -