Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Image from 'next/image';
import projectCow from '/public/judges/projects/project-cow.svg';
import twoStars from '/public/judges/projects/two_stars.svg';

export default function ProjectsEmptyState({
title,
Expand All @@ -9,13 +10,14 @@ export default function ProjectsEmptyState({
subtitle: string;
}) {
return (
<div className="flex mt-[65px] flex-col items-center h-[calc(100vh-100px)] bg-[#F2F2F7] text-center">
<div className="flex flex-col items-center text-center">
<div className="flex w-full justify-end pr-[25%]">
<Image src={twoStars} alt="Two Stars" />
</div>
<span className="text-[32px] font-[700] text-[#000000] mb-[12px]">
{title}
</span>
<span className="text-[16px] font-[500] text-[#000000] whitespace-pre-line mb-[32px]">
{subtitle}
</span>
<span className="text-[16px] font-[500] text-[#000000]">{subtitle}</span>
<Image src={projectCow} alt="Project Cow" />
</div>
);
Expand Down
140 changes: 140 additions & 0 deletions app/(pages)/judges/(app)/_components/Projects/ProjectPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
'use client';
import { useEffect, useState } from 'react';
import { useSession } from 'next-auth/react';
import { FaChevronLeft } from 'react-icons/fa6';
import { useJudgeSubmissions } from '@pages/_hooks/useJudgeSubmissions';
import UnscoredPage from './UnscoredPage';
import ScoredPage from './ScoredPage';
import Link from 'next/link';
import Loader from '@pages/_components/Loader/Loader';
import ProjectTab from './ProjectTab';
import Team from '@typeDefs/team';

interface ButtonProps {
text: string;
isSelected: boolean;
badgeCount?: number;
onClick: () => void;
}

const Button: React.FC<ButtonProps> = ({
text,
isSelected,
badgeCount,
onClick,
}) => (
<div className="relative">
<button
onClick={onClick}
className={`px-[24px] py-[12px] rounded-full text-[18px] font-semibold transition-colors ${
isSelected
? 'bg-[#3D3D3D] text-white border-[1px] border-[#3D3D3D]'
: 'bg-transparent text-[#3D3D3D] border-[1px] border-dashed border-[#AAAAAA]'
}`}
>
{text}
</button>
{badgeCount !== undefined && (
<span className="absolute -top-[10px] -right-[10px] bg-[#F4847A] text-white text-[14px] font-bold w-[28px] h-[28px] rounded-full flex items-center justify-center">
{badgeCount}
</span>
)}
</div>
);

const ProjectPage = () => {
const [selectedButton, setSelectedButton] = useState<
'Unjudged' | 'Scored' | 'Missing Team'
>('Unjudged');
const { data: session } = useSession();
const user = session?.user;
const userId = user?.id;

const { scoredTeams, unscoredTeams, loading, error, fetchSubmissions } =
useJudgeSubmissions(userId);

const allUnscoredTeams = (unscoredTeams ?? []) as Team[];
const missingTeams = allUnscoredTeams.filter((team) =>
(team.reports ?? []).some((report) => report.judge_id === userId)
);
const visibleUnscoredTeams = allUnscoredTeams.filter(
(team) => !(team.reports ?? []).some((report) => report.judge_id === userId)
);

useEffect(() => {
if (selectedButton === 'Missing Team' && missingTeams.length === 0) {
setSelectedButton('Unjudged');
}
}, [missingTeams.length, selectedButton]);

if (loading) {
return <Loader />;
}

if (error) {
return error;
}

return (
<div className="flex flex-col h-full my-[40px] mx-[24px]">
<Link href="/judges" className="flex items-center my-[12px] gap-[12px]">
<FaChevronLeft fill="#005271" height={8.48} width={4.24} />
<span className="font-semibold text-[18px] text-[#121212]">
Back to home
</span>
</Link>

<div className="my-[12px]">
<span className="font-bold text-[48px] text-black block">Projects</span>
</div>

<div className="flex gap-[8px] mb-[32px] overflow-x-auto pt-[12px] pb-2 no-scrollbar">
<div className="flex gap-[8px] flex-nowrap min-w-max">
<Button
text="Unjudged"
isSelected={selectedButton === 'Unjudged'}
badgeCount={visibleUnscoredTeams.length}
onClick={() => setSelectedButton('Unjudged')}
/>
<Button
text="Scored"
isSelected={selectedButton === 'Scored'}
onClick={() => setSelectedButton('Scored')}
/>
{missingTeams.length > 0 && (
<Button
text="Missing Team"
isSelected={selectedButton === 'Missing Team'}
onClick={() => setSelectedButton('Missing Team')}
/>
)}
</div>
</div>

<div>
{loading ? (
<Loader />
) : error ? (
<div className="bg-red-100 p-4 rounded">
<p className="text-red-800">{error}</p>
</div>
) : selectedButton === 'Unjudged' ? (
<UnscoredPage
teams={visibleUnscoredTeams}
revalidateData={() => fetchSubmissions()}
/>
) : selectedButton === 'Missing Team' ? (
<div className="flex flex-col gap-[24px]">
{missingTeams.map((team) => (
<ProjectTab key={team._id} team={team} disabled />
))}
</div>
) : (
<ScoredPage teams={scoredTeams} />
)}
</div>
</div>
);
};

export default ProjectPage;
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ const ProjectTab: React.FC<ProjectTabProps> = ({ team, disabled }) => {
return (
<Link
href={`/judges/score/${team._id}`}
className="flex items-center justify-center bg-white rounded-[16px] gap-[24px] py-[20px]"
className="flex items-center justify-center bg-[#F3F3FC] rounded-[24px] gap-[14px] py-[12px] px-[24px]"
style={{
pointerEvents: disabled ? 'none' : 'auto',
}}
>
<span className="text-[48px] text-[#000000] leading-[60px] font-[600]">
<span className="text-[32px] text-[#707070] font-semibold">
{team.tableNumber}
</span>
<span className="max-w-[137px] break-words text-[24px] text-[#000000] tracking-[0.48px] leading-[30px] font-[500]">
<span className="text-[18px] text-[#707070] font-semibold">
{team.name}
</span>
</Link>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface ScoredPageProps {

const ScoredPage = ({ teams }: ScoredPageProps) => {
return (
<div className="flex flex-col mt-[4px] gap-[16px] mb-[120px]">
<div className="flex flex-col">
{teams.length === 0 ? (
<ProjectsEmptyState
title="Let's begin!"
Expand Down
199 changes: 199 additions & 0 deletions app/(pages)/judges/(app)/_components/Projects/UnscoredPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { useState } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { useSession } from 'next-auth/react';
import ProjectTab from './ProjectTab';
import Team from '@typeDefs/team';
import { reportMissingProject } from '@actions/teams/reportMissingTeam';
import ReportModal from './ReportModal';
import EmptyState from './EmptyState';
import { FaChevronRight } from 'react-icons/fa6';
import { IoExpandOutline } from 'react-icons/io5';

import venueMap from '@public/judges/projects/venueMap2026.svg';
import closeIcon from '@public/judges/projects/x.svg';

interface UnscoredPageProps {
teams: Team[];
revalidateData: () => void;
}

export default function UnscoredPage({
teams,
revalidateData,
}: UnscoredPageProps) {
const { data: session } = useSession();
const user = session?.user;
const judgeId = user?.id ?? '';
const [expandReportButton, setExpandReportButton] = useState(false);
const [mapExpanded, setMapExpanded] = useState(false);
const [modalStage, setModalStage] = useState<
'hidden' | 'loading' | 'success' | 'error'
>('hidden');
const [errorMsg, setErrorMsg] = useState<string | null>(null);

if (teams.length === 0) {
return (
<EmptyState
title="You're Done!"
subtitle={"You've judged all your projects.\n Thank you so much!"}
/>
);
}

const currentTeam = teams[0];
const upcomingTeams = teams.slice(1);

const handleTeamReport = async (team: Team) => {
setModalStage('loading');
const reportRes = await reportMissingProject(judgeId, team._id ?? '');
if (!reportRes.ok) {
setErrorMsg((reportRes.error ?? '').slice(0, 100));
setModalStage('error');
} else {
setErrorMsg(null);
setModalStage('success');
revalidateData();
}
setExpandReportButton(false);
};

return (
<>
<div className="flex flex-col">
<div className="bg-white px-[30px] py-[34px] rounded-[32px]">
{/* Current Project header */}
<div className="flex flex-col gap-[7px]">
<p className="text-[22px] font-semibold text-[#3F3F3F]">
Current Project
</p>
<p className="text-[16px] text-[#5E5E65]">
Projects must be judged in order one by one order.
</p>
</div>

{/* To-score Project Button */}
<Link
href={`/judges/score/${currentTeam._id}`}
className="bg-black rounded-[24px] flex items-center justify-between px-[24px] py-[12px] my-[16px]"
>
<div className="flex items-center gap-[14px]">
<span className="text-white font-semibold text-[32px]">
{currentTeam.tableNumber || currentTeam._id}
</span>
<span className="text-white font-semibold text-[18px]">
{currentTeam.name || `Team ${currentTeam._id}`}
</span>
</div>
<FaChevronRight className="text-white" size={18} />
</Link>

{/* Map card */}
<div className="relative w-full mt-[36px] rounded-[20px] border-[1.5px] border-[#E0E0E0] overflow-visible mb-[4px]">
<div className="flex p-[12px] rounded-[20px] overflow-hidden">
<Image src={venueMap} alt="first floor map" />
</div>
<button
onClick={() => {
setMapExpanded(true);
}}
className="absolute bottom-[-26px] left-1/2 -translate-x-1/2 bg-black text-white rounded-full w-[52px] h-[52px] flex items-center justify-center z-10"
>
<IoExpandOutline size={24} />
</button>
</div>

{/* Flag section */}
<div className="flex flex-col items-center gap-[14px] mt-[44px]">
<p className="text-[16px] text-[#6B6B6B] text-center leading-snug">
If the team you are judging is not present, tap the{' '}
<span className="text-[#F4847A] font-semibold">red button</span>{' '}
below.
</p>

{/* TODO: TURN INTO POPUP */}
{expandReportButton ? (
<>
<div className="text-[#3F3F3F] font-semibold text-[22px] text-left">
Are you sure?
</div>
<div className="flex w-full gap-[10px]">
<button
onClick={() => handleTeamReport(currentTeam)}
className="flex-1 h-[56px] bg-[#F4847A] rounded-full text-white font-semibold text-[16px]"
>
Yes
</button>
<button
onClick={() => setExpandReportButton(false)}
className="flex-1 h-[56px] border-[1.5px] border-[#AAAAAA] rounded-full text-[#3D3D3D] font-semibold text-[16px]"
>
Cancel
</button>
</div>
</>
) : (
<button
onClick={() => setExpandReportButton(true)}
className="w-full h-[56px] bg-[#F4847A] rounded-full text-white font-semibold text-[18px]"
>
Flag team as missing
</button>
)}
</div>
</div>

{/* Next up */}
{upcomingTeams.length > 0 && (
<div className="flex flex-col mt-[32px] gap-[24px]">
<span className="text-[22px] font-semibold text-black">
Next up
</span>
<div className="flex flex-col gap-[24px]">
{upcomingTeams.map((team) => (
<ProjectTab key={team._id} team={team} disabled />
))}
</div>
</div>
)}

{/* Expanded Map Modal */}
{mapExpanded && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-[22px] py-[48px]"
onClick={() => setMapExpanded(false)}
>
<div
className="bg-white rounded-[24px] overflow-hidden w-full max-w-[430px] h-[calc(100dvh-96px)] relative flex flex-col"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={() => setMapExpanded(false)}
className="absolute top-[16px] right-[16px] z-10 bg-black text-white rounded-full w-[36px] h-[36px] flex items-center justify-center text-lg font-bold"
aria-label="Close map"
>
<Image src={closeIcon} alt="Close" width={15} height={15} />
</button>

<div className="h-full w-full overflow-auto px-[16px] py-[16px]">
<div className="flex h-full min-w-full items-center justify-center rotate-90 m-[150px]">
<Image
src={venueMap}
alt="first floor map"
className="h-full w-auto max-w-none select-none"
/>
</div>
</div>
</div>
</div>
)}

<ReportModal
modalStage={modalStage}
setModalStage={setModalStage}
errorMsg={errorMsg}
/>
</div>
</>
);
}
Loading
Loading