diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 32452a1..c9bc3f6 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -14,7 +14,7 @@ jobs:
- uses: actions/setup-node@v3
- working-directory: backend # Change this to the name of your backend directory
run: |
- npm ci
+ npm ci --legacy-peer-deps
npm run lint-check
frontend:
name: Frontend lint and style check
@@ -24,5 +24,5 @@ jobs:
- uses: actions/setup-node@v3
- working-directory: frontend # Change this to the name of your frontend directory
run: |
- npm ci
+ npm ci --legacy-peer-deps
npm run lint-check
diff --git a/frontend/app/submit/page.tsx b/frontend/app/submit/page.tsx
index b2ad93f..e8f3678 100644
--- a/frontend/app/submit/page.tsx
+++ b/frontend/app/submit/page.tsx
@@ -1,3 +1,4 @@
+import SubmissionForm from "../../src/components/SubmissionForm";
// =============================================================================
// SCREEN: Social Submission Form (/submit)
// =============================================================================
@@ -38,17 +39,7 @@
export default function SubmitPage() {
return (
- Submit a Social
- {/* TODO: Field 1 — Submission name (text input) */}
- {/* TODO: Field 2 — Activity type (text input) */}
- {/* TODO: Field 3 — Who attended (multi-select dropdown, fetch from /api/members) */}
- {/* TODO: Field 4 — Date & time (date + time picker) */}
- {/* TODO: Field 5 — Photo upload (required) */}
- {/* TODO: Field 6 — Point assignment per team */}
- {/* TODO: Field 7 — PVP flag (if applicable) */}
- {/* TODO: Validate min 3 attendees before submit */}
- {/* TODO: POST to /api/submissions, then redirect to /submit/confirmation */}
- Submission form goes here.
+
);
}
diff --git a/frontend/public/img/flag.svg b/frontend/public/img/flag.svg
new file mode 100644
index 0000000..43b1ffe
--- /dev/null
+++ b/frontend/public/img/flag.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/src/components/PointAllocation.tsx b/frontend/src/components/PointAllocation.tsx
new file mode 100644
index 0000000..e9353b3
--- /dev/null
+++ b/frontend/src/components/PointAllocation.tsx
@@ -0,0 +1,215 @@
+"use client";
+
+import Image from "next/image";
+
+import teamChipStyles from "../styles/teamChip.module.css";
+
+import TeamChip from "./TeamChip";
+
+type Team = {
+ id: string;
+ name: string;
+ members?: number;
+};
+
+type Props = {
+ teams: Team[];
+ onNext?: () => void;
+};
+
+export default function PointAllocation({ teams, onNext }: Props) {
+ const teamById = Object.fromEntries(teams.map((team) => [team.id, team]));
+ const teamPairs: [Team, Team | undefined][] = [];
+
+ if (teamById.dbc && teamById.pvp) {
+ teamPairs.push([teamById.dbc, teamById.pvp]);
+ } else if (teamById.dbc) {
+ teamPairs.push([teamById.dbc, undefined]);
+ } else if (teamById.pvp) {
+ teamPairs.push([teamById.pvp, undefined]);
+ }
+
+ if (teamById.f3) {
+ teamPairs.push([teamById.f3, undefined]);
+ }
+
+ if (teamById.homestart) {
+ teamPairs.push([teamById.homestart, undefined]);
+ }
+
+ if (teamById.test) {
+ teamPairs.push([teamById.test, undefined]);
+ }
+
+ return (
+
+
+ Point Allocation
+
+
+ {/* Rows with team pairs and scores */}
+
+ {teamPairs.map((pair, pairIndex) => (
+
+ {/* First team chip */}
+
+
+ {/* Second team chip (if exists) */}
+ {pair[1] &&
}
+
+ {/* Spacer */}
+
+
+ {/* 1+1 = label */}
+
+ 1+1 =
+
+
+ {/* Score bubble */}
+
+ 2
+
+
+ {/* pts label */}
+
+ pts
+
+
+ ))}
+
+
+
+
+
+
+ Status: Flagged
+
+
+
+ {/* Next Button */}
+
+ Next
+
+
+
+ );
+}
diff --git a/frontend/src/components/SubmissionForm.tsx b/frontend/src/components/SubmissionForm.tsx
new file mode 100644
index 0000000..26e6a4d
--- /dev/null
+++ b/frontend/src/components/SubmissionForm.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+import PointAllocation from "./PointAllocation";
+import TeamHosting from "./TeamHosting";
+
+type Team = {
+ id: string;
+ name: string;
+};
+
+export default function SubmissionForm() {
+ const [teams, setTeams] = useState([
+ { id: "dbc", name: "DBC" },
+ { id: "f3", name: "F3" },
+ { id: "homestart", name: "Homestart" },
+ { id: "test", name: "TEST" },
+ { id: "pvp", name: "PVP" },
+ ]);
+ const visibleTeamIds = new Set(["dbc", "homestart", "pvp"]);
+ const visibleTeams = teams.filter((team) => visibleTeamIds.has(team.id));
+
+ useEffect(() => {
+ const fetchTeams = async () => {
+ try {
+ const response = await fetch("/api/teams");
+ if (response.ok) {
+ const data = (await response.json()) as Team[];
+ setTeams(data);
+ }
+ } catch (err) {
+ console.error("Failed to fetch teams", err);
+ }
+ };
+
+ void fetchTeams();
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/TeamChip.tsx b/frontend/src/components/TeamChip.tsx
new file mode 100644
index 0000000..c245520
--- /dev/null
+++ b/frontend/src/components/TeamChip.tsx
@@ -0,0 +1,20 @@
+import teamChipStyles from "../styles/teamChip.module.css";
+
+type Team = {
+ id: string;
+ name: string;
+};
+
+type TeamChipProps = {
+ team: Team;
+};
+
+export default function TeamChip({ team }: TeamChipProps) {
+ const variantClassName = teamChipStyles[team.id as keyof typeof teamChipStyles] ?? "";
+ const className = [teamChipStyles.teamChip, variantClassName].filter(Boolean).join(" ");
+ return (
+
+ {team.name}
+
+ );
+}
diff --git a/frontend/src/components/TeamHosting.tsx b/frontend/src/components/TeamHosting.tsx
new file mode 100644
index 0000000..3a317d3
--- /dev/null
+++ b/frontend/src/components/TeamHosting.tsx
@@ -0,0 +1,144 @@
+"use client";
+import teamChipStyles from "../styles/teamChip.module.css";
+
+import TeamChip from "./TeamChip";
+
+type Team = {
+ id: string;
+ name: string;
+};
+
+type Props = {
+ teams: Team[];
+};
+
+export default function TeamsHosting({ teams }: Props) {
+ const featuredTeams = ["dbc", "homestart"]
+ .map((teamId) => teams.find((team) => team.id === teamId))
+ .filter((team): team is Team => Boolean(team));
+
+ return (
+
+
+ Teams Hosting
+
+
+
+ {teams.map((team) => (
+
+ ))}
+
+
+
+
+ If teams are missing, go back and tag people from those teams. Points will be split by
+ headcount.
+
+
+
+
+
+
+
+
+ {featuredTeams.map((team) => (
+
+
+
+
+ {team.id === "dbc" ? 1 : 2}
+
+
+ members
+
+
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/styles/teamChip.module.css b/frontend/src/styles/teamChip.module.css
new file mode 100644
index 0000000..2832913
--- /dev/null
+++ b/frontend/src/styles/teamChip.module.css
@@ -0,0 +1,90 @@
+.teamChip {
+ --team-chip-width: 88px;
+ --team-chip-height: 31px;
+ width: var(--team-chip-width);
+ height: var(--team-chip-height);
+ padding: 8px 31px 7px;
+ gap: 10px;
+ border-radius: 100px;
+ background: var(--color-background-primary);
+ color: var(--color-text-primary);
+ font-family: Rubik, sans-serif;
+ font-size: 13px;
+ font-weight: 500;
+ line-height: 100%;
+ border: 2px solid var(--team-chip-border, #d8eaf2);
+ white-space: nowrap;
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ appearance: none;
+ -webkit-appearance: none;
+}
+
+.label {
+ font-size: calc(var(--team-chip-height) * 0.42);
+ line-height: 1;
+ transform: rotate(0deg);
+ opacity: 1;
+ color: var(--Text-Black, #1f1f1f);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ max-width: 100%;
+}
+
+.dbc {
+ --team-chip-border: #c87bff;
+}
+
+.f3 {
+ --team-chip-border: #1f4f9f;
+}
+
+.homestart {
+ --team-chip-border: #6bdb0f;
+}
+
+.test {
+ --team-chip-border: #f0c400;
+}
+
+.pvp {
+ --team-chip-border: #4c5c86;
+}
+
+.scoreBubble {
+ width: 37px;
+ height: 31px;
+ border-radius: 15px;
+ border: 1px solid transparent;
+ background: var(--score-bubble-color, #7dd216);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.dbcScore {
+ --score-bubble-color: #c87bff;
+}
+
+.f3Score {
+ --score-bubble-color: #1f4f9f;
+}
+
+.homestartScore {
+ --score-bubble-color: #6bdb0f;
+}
+
+.testScore {
+ --score-bubble-color: #ffd000f6;
+}
+
+.pvpScore {
+ --score-bubble-color: #f0c400;
+}