{/* mobile menu */}
diff --git a/frontend2/src/components/JoinTeam.tsx b/frontend2/src/components/JoinTeam.tsx
new file mode 100644
index 000000000..843bd66e4
--- /dev/null
+++ b/frontend2/src/components/JoinTeam.tsx
@@ -0,0 +1,7 @@
+import React from "react";
+
+const JoinTeam: React.FC = () => {
+ return
you have no team! this page is tbd
;
+};
+
+export default JoinTeam;
diff --git a/frontend2/src/components/SectionCard.tsx b/frontend2/src/components/SectionCard.tsx
new file mode 100644
index 000000000..3111dcb0c
--- /dev/null
+++ b/frontend2/src/components/SectionCard.tsx
@@ -0,0 +1,27 @@
+import React from "react";
+
+interface SectionCardProps {
+ children?: React.ReactNode;
+ title?: string;
+ className?: string;
+}
+
+const SectionCard: React.FC
= ({
+ children,
+ title,
+ className = "",
+}) => {
+ return (
+
+ {title !== undefined && (
+
{title}
+ )}
+
+
+ );
+};
+
+export default SectionCard;
diff --git a/frontend2/src/components/elements/DescriptiveCheckbox.tsx b/frontend2/src/components/elements/DescriptiveCheckbox.tsx
new file mode 100644
index 000000000..f9045f352
--- /dev/null
+++ b/frontend2/src/components/elements/DescriptiveCheckbox.tsx
@@ -0,0 +1,46 @@
+import React from "react";
+import Icon from "./Icon";
+import { Switch } from "@headlessui/react";
+
+interface DescriptiveCheckboxProps {
+ checked: boolean;
+ onChange: (checked: boolean) => void;
+ title: string;
+ description: string;
+}
+
+const DescriptiveCheckbox: React.FC = ({
+ checked,
+ onChange,
+ title,
+ description,
+}) => {
+ return (
+
+
+
{title}
+
+ {description}
+
+
+
+
+
+
+ );
+};
+
+export default DescriptiveCheckbox;
diff --git a/frontend2/src/components/elements/FormLabel.tsx b/frontend2/src/components/elements/FormLabel.tsx
index 1f7b76f97..1aa044717 100644
--- a/frontend2/src/components/elements/FormLabel.tsx
+++ b/frontend2/src/components/elements/FormLabel.tsx
@@ -7,7 +7,7 @@ const FormLabel: React.FC<{
}> = ({ label, required = false, className }) => {
return (
diff --git a/frontend2/src/components/elements/Input.tsx b/frontend2/src/components/elements/Input.tsx
index 0489d7544..10e0739fd 100644
--- a/frontend2/src/components/elements/Input.tsx
+++ b/frontend2/src/components/elements/Input.tsx
@@ -22,10 +22,15 @@ const Input = forwardRef(function Input(
diff --git a/frontend2/src/components/elements/SelectMenu.tsx b/frontend2/src/components/elements/SelectMenu.tsx
index 9883aacb1..4bda266bb 100644
--- a/frontend2/src/components/elements/SelectMenu.tsx
+++ b/frontend2/src/components/elements/SelectMenu.tsx
@@ -62,7 +62,7 @@ function SelectMenu({
leaveTo="opacity-0"
>
diff --git a/frontend2/src/components/elements/TextArea.tsx b/frontend2/src/components/elements/TextArea.tsx
new file mode 100644
index 000000000..0e8d0e4c1
--- /dev/null
+++ b/frontend2/src/components/elements/TextArea.tsx
@@ -0,0 +1,38 @@
+import React, { forwardRef } from "react";
+import FormError from "./FormError";
+import FormLabel from "./FormLabel";
+
+interface TextAreaProps extends React.ComponentPropsWithoutRef<"textarea"> {
+ label?: string;
+ required?: boolean;
+ className?: string;
+ errorMessage?: string;
+}
+
+const Input = forwardRef(function Input(
+ { label, required = false, className = "", errorMessage, ...rest },
+ ref,
+) {
+ const invalid = errorMessage !== undefined;
+ return (
+
+
+
+ );
+});
+
+export default Input;
diff --git a/frontend2/src/components/team/MemberList.tsx b/frontend2/src/components/team/MemberList.tsx
new file mode 100644
index 000000000..510e36e79
--- /dev/null
+++ b/frontend2/src/components/team/MemberList.tsx
@@ -0,0 +1,57 @@
+import React from "react";
+import { type UserPublic } from "../../utils/types";
+import { useCurrentUser } from "../../contexts/CurrentUserContext";
+
+interface MemberListProps {
+ members: UserPublic[];
+ className?: string;
+}
+
+interface UserRowProps {
+ user: UserPublic;
+ isCurrentUser?: boolean;
+}
+const UserRow: React.FC = ({ user, isCurrentUser = false }) => {
+ return (
+
+
+
+ {user.username}
+ {isCurrentUser && (
+ (you)
+ )}
+ {user.is_staff && (
+ (staff)
+ )}
+
+
+ {user.profile?.school}
+
+
+ );
+};
+
+// Displays a list of the users in members. If the current user is in members,
+// displays the current user first.
+const MemberList: React.FC = ({ members, className = "" }) => {
+ const { user: currentUser } = useCurrentUser();
+ return (
+
+ {/* display current user first */}
+ {currentUser !== undefined && (
+
+ )}
+ {members.map(
+ (member) =>
+ member.id !== currentUser?.id && (
+
+ ),
+ )}
+
+ );
+};
+
+export default MemberList;
diff --git a/frontend2/src/contexts/CurrentTeamContext.ts b/frontend2/src/contexts/CurrentTeamContext.ts
index 567f02b28..a8e9db873 100644
--- a/frontend2/src/contexts/CurrentTeamContext.ts
+++ b/frontend2/src/contexts/CurrentTeamContext.ts
@@ -2,6 +2,8 @@ import { createContext, useContext } from "react";
import { type TeamPrivate } from "../utils/types";
export enum TeamStateEnum {
+ // the current team state is still loading
+ LOADING = "loading",
// the current user is not part of a team (or is not logged in)
NO_TEAM = "no_team",
// the current user is part of a team
diff --git a/frontend2/src/contexts/CurrentTeamProvider.tsx b/frontend2/src/contexts/CurrentTeamProvider.tsx
index d8f3172c1..f18a0b4be 100644
--- a/frontend2/src/contexts/CurrentTeamProvider.tsx
+++ b/frontend2/src/contexts/CurrentTeamProvider.tsx
@@ -8,11 +8,13 @@ import { retrieveTeam } from "../utils/api/team";
export const CurrentTeamProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
+ // invariant: team must be undefined when teamState is LOADING or NO_TEAM
+ // team must be defined when teamState is IN_TEAM
const [teamData, setTeamData] = useState<{
team?: TeamPrivate;
teamState: TeamStateEnum;
}>({
- teamState: TeamStateEnum.NO_TEAM,
+ teamState: TeamStateEnum.LOADING,
});
const { authState } = useCurrentUser();
const { episodeId } = useEpisodeId();
diff --git a/frontend2/src/index.css b/frontend2/src/index.css
index 8ff36ee89..aa3eb228e 100644
--- a/frontend2/src/index.css
+++ b/frontend2/src/index.css
@@ -1,20 +1,20 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
-@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Josefin+Sans:wght@100;300;400;500;700&display=swap");
+@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Josefin+Sans:wght@100;300;400;500;700&display=swap");
@layer base {
h1 {
- @apply pb-4 text-3xl font-medium text-gray-900;
+ @apply pb-4 text-3xl font-semibold text-gray-900;
}
h2 {
- @apply pb-4 pt-6 text-2xl font-medium text-gray-900;
+ @apply pb-4 pt-6 text-2xl font-semibold text-gray-900;
}
h3 {
- @apply pb-4 text-xl font-medium text-gray-900;
+ @apply pb-4 text-xl font-semibold text-gray-900;
}
h4 {
- @apply pb-4 text-lg font-medium text-gray-700;
+ @apply pb-4 text-lg font-semibold text-gray-700;
}
p {
@apply pb-4 text-gray-900;
diff --git a/frontend2/src/views/MyTeam.tsx b/frontend2/src/views/MyTeam.tsx
new file mode 100644
index 000000000..9e4538345
--- /dev/null
+++ b/frontend2/src/views/MyTeam.tsx
@@ -0,0 +1,150 @@
+import React, { useMemo, useState } from "react";
+import { PageTitle } from "../components/elements/BattlecodeStyle";
+import SectionCard from "../components/SectionCard";
+import Input from "../components/elements/Input";
+import TextArea from "../components/elements/TextArea";
+import { TeamStateEnum, useCurrentTeam } from "../contexts/CurrentTeamContext";
+import Button from "../components/elements/Button";
+import MemberList from "../components/team/MemberList";
+import DescriptiveCheckbox from "../components/elements/DescriptiveCheckbox";
+import JoinTeam from "../components/JoinTeam";
+
+const MyTeam: React.FC = () => {
+ const { team, teamState } = useCurrentTeam();
+ const [checked, setChecked] = useState(false);
+ const membersList = useMemo(() => {
+ return (
+
+ {team !== undefined && }
+
+
+ );
+ }, [team]);
+ if (teamState !== TeamStateEnum.IN_TEAM || team === undefined) {
+ return ;
+ }
+ return (
+
+
Team Settings
+
+
+
+
+
+
+
+ {team.name}
+
+
+
+
+
+
+ {/* TODO: This button will be disabled when no changes have been made,
+ and will turn dark cyan after changes have been made. When changes
+ are saved, it will be disabled again. We may want to show a popup indicating
+ success */}
+
+
+
+
+ {/* The members list that displays when on a smaller screen */}
+
+ {membersList}
+
+
+
+
+
+ Check the box(es) that apply to all members of your
+ team.
+
+
+ This determines which tournaments and prizes your team is
+ eligible for.
+
+
+
+ {
+ setChecked(checked);
+ }}
+ title="Student"
+ description="Here's a description of what a student is"
+ />
+ {
+ setChecked(checked);
+ }}
+ title="US Student"
+ description="Here's a description of what a US student is"
+ />
+ {
+ setChecked(checked);
+ }}
+ title="High school student"
+ description="These descriptions and titles should be loaded dynamically."
+ />
+ {
+ setChecked(checked);
+ }}
+ title="Newbie"
+ description="These descriptions and titles should be loaded dynamically. I think this is MIT newbie? we might want to rename this criteria"
+ />
+
+
+
+
+
+
+
+ Choose how you want to handle incoming scrimmage requests from
+ other players.
+
+
+
+ {
+ setChecked(checked);
+ }}
+ title="Auto-accept ranked scrimmages"
+ description="When enabled, your team will automatically accept
+ ranked scrimmage requests. Ranked scrimmages affect your ELO rating."
+ />
+ {
+ setChecked(checked);
+ }}
+ title="Auto-accept unranked scrimmages"
+ description="When enabled, your team will automatically accept
+ unranked scrimmage requests. Unranked scrimmages do not affect your ELO rating."
+ />
+
+
+
+
+ {/* The members list that displays to the right side when on a big screen. */}
+
+ {membersList}
+
+
+
+ );
+};
+
+export default MyTeam;