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 + +
+ ))} +
+ +
+
+ Flag + + Status: Flagged + +
+ + {/* Next Button */} + +
+
+ ); +} 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; +}