Skip to content

Commit

Permalink
Add leaderboard groups: frontend portion (#94)
Browse files Browse the repository at this point in the history
* Revert "Revert "Add leaderboard groups to front-end""

This reverts commit 9d2bfbb.

* undo these
  • Loading branch information
Bentechy66 authored Oct 8, 2023
1 parent 9d2bfbb commit 96608bd
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 19 deletions.
1 change: 1 addition & 0 deletions src/@ractf/api/consts.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const ENDPOINTS = {
TEAM_CREATE: "/team/create/",
TEAM_JOIN: "/team/join/",
TEAM_LEAVE: "/team/leave/",
LEADERBOARD_GROUPS: "/team/groups/",

LEADERBOARD_GRAPH: "/leaderboard/graph/",
LEADERBOARD_USER: "/leaderboard/user/",
Expand Down
4 changes: 2 additions & 2 deletions src/@ractf/api/team.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ import { reloadAll } from "./reloadAll";

export const modifyTeam = (teamId, data) => http.patch(ENDPOINTS.TEAM + teamId, data);

export const createTeam = (name, password) => {
export const createTeam = (name, password, leaderboard_group) => {
return new Promise((resolve, reject) => {
http.post(ENDPOINTS.TEAM_CREATE, { name, password }).then(async data => {
http.post(ENDPOINTS.TEAM_CREATE, { name, password, leaderboard_group }).then(async data => {
const team = await http.get("/team/self");
store.dispatch(actions.setTeam(team));
resolve(data);
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
"no_bio": "No bio set",
"solve": "{{count}} point - Scored by {{user_name}}",
"solve_plural": "{{count}} points - Scored by {{user_name}}",
"already_in_team": "You are already in a team."
"already_in_team": "You are already in a team.",
"no_leaderboard_group_tab": "No group set",
"all_leaderboard_groups": "All teams"
},
"profile": {
"no_bio": "No bio set",
Expand Down
63 changes: 51 additions & 12 deletions src/pages/Leaderboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";

import {
Button, Graph, Tab, Table, Page, PageHead, Container
Button, Graph, Tab, Table, Page, PageHead, Container,
TabbedView
} from "@ractf/ui-kit";
import { useApi, usePaginated } from "@ractf/util/http";
import { ENDPOINTS } from "@ractf/api";
Expand All @@ -36,6 +37,7 @@ const Leaderboard = React.memo(() => {
const { t } = useTranslation();

const [graph, , , refreshGraph] = useApi(ENDPOINTS.LEADERBOARD_GRAPH);
const [groups, , gRefresh] = usePaginated(ENDPOINTS.LEADERBOARD_GROUPS);
const [uState, uNext, uRefresh] = usePaginated(ENDPOINTS.LEADERBOARD_USER);
const [tState, tNext, tRefresh] = usePaginated(ENDPOINTS.LEADERBOARD_TEAM);
const start_time = useConfig("start_time");
Expand Down Expand Up @@ -78,7 +80,10 @@ const Leaderboard = React.memo(() => {

if (!teamPlots.hasOwnProperty(id)) {
teamPlots[id] = {
data: [{ x: minTime, y: 0 }], label: i.team_name
data:
[{ x: minTime, y: 0 }],
label: i.team_name,
leaderboard_group_name: i.leaderboard_group_name ?? t("teams.no_leaderboard_group_tab")
};
points[id] = 0;
}
Expand All @@ -92,13 +97,14 @@ const Leaderboard = React.memo(() => {
setTeamGraphData(
Object.values(teamPlots).sort((a, b) => points[b.id] - points[a.id])
);
}, [graph, start_time, hasTeams]);
}, [graph, start_time, hasTeams, t]);

useInterval(() => {
if (!liveReload) return;
refreshGraph();
uRefresh();
tRefresh();
gRefresh();
}, 10000);

const userData = (lbdata) => {
Expand All @@ -116,15 +122,48 @@ const Leaderboard = React.memo(() => {
]);
};

const teamTab = <>
{teamGraphData && teamGraphData.length > 0 && (
<Graph key="teams" data={teamGraphData} timeGraph noAnimate />
)}
<Table noSort headings={["Place", t("team"), t("point_plural")]} data={teamData(tState.data)} />
{tState.hasMore && <Container full centre>
<Button disabled={tState.loading} onClick={() => {tNext();}}>{t("load_more")}</Button>
</Container>}
</>;
const defaultTabContent = (
<>
{teamGraphData && teamGraphData.length > 0 && <Graph key="teams" data={teamGraphData} timeGraph noAnimate />}
<Table noSort headings={["Place", t("team"), t("point_plural")]} data={teamData(tState.data)} />
{tState.hasMore && (
<Container full centre>
<Button disabled={tState.loading} onClick={() => { tNext(); }}>{t("load_more")}</Button>
</Container>
)}
</>
);

const teamTab = (
<>
{groups.data && groups.data.length > 1 ? (
<TabbedView center initial={0}>
<Tab label={t("teams.all_leaderboard_groups")}>
{defaultTabContent}
</Tab>
{groups.data.map(g => {
const filteredGraphData = teamGraphData.filter(i => i.leaderboard_group_name === g.name);
const filteredTeamData = tState.data.filter(i => i.leaderboard_group_name === g.name);

return (
<Tab label={g.name} key={g.name}>
{filteredGraphData && filteredGraphData.length > 0 && <Graph key="teams" data={filteredGraphData} timeGraph noAnimate />}
<Table noSort headings={["Place", t("team"), t("point_plural")]} data={teamData(filteredTeamData)} />
{tState.hasMore && (
<Container full centre>
<Button disabled={tState.loading} onClick={() => { tNext(); }}>{t("load_more")}</Button>
</Container>
)}
</Tab>
);
})}
</TabbedView>
) : (
<>{defaultTabContent}</>
)}
</>
);

const userTab = <>
{userGraphData && userGraphData.length > 0 && (
<Graph key="users" data={userGraphData} timeGraph noAnimate />
Expand Down
15 changes: 11 additions & 4 deletions src/plugins/base/auth/components/Teams.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";

import {
Form, HR, Input, Button, SubtleText, Container
Form, HR, Input, Button, SubtleText, Container, Select
} from "@ractf/ui-kit";
import { joinTeam, createTeam, reloadAll } from "@ractf/api";
import { useConfig } from "@ractf/shell-util";
import * as http from "@ractf/util/http";
import { usePaginated } from "@ractf/util/http";
import { ENDPOINTS, joinTeam, createTeam, reloadAll } from "@ractf/api";

import Link from "components/Link";

Expand Down Expand Up @@ -100,6 +101,7 @@ export const CreateTeam = () => {

const [message, setMessage] = useState("");
const [success, setSuccess] = useState(false);
const [groups, , gRefresh] = usePaginated(ENDPOINTS.LEADERBOARD_GROUPS);
const [locked, setLocked] = useState(false);
const team = useSelector(state => state.team);
const hasTeams = useConfig("enable_teams");
Expand All @@ -110,14 +112,14 @@ export const CreateTeam = () => {
if (team !== null)
return <Redirect to={"/team/me"} />;

const doCreateTeam = ({ name, password }) => {
const doCreateTeam = ({ name, password, leaderboard_group }) => {
if (!name.length)
return setMessage(t("team_wiz.name_missing"));
if (password.length < 8)
return setMessage(t("team_wiz.pass_short"));

setLocked(true);
createTeam(name, password).then(resp => {
createTeam(name, password, leaderboard_group).then(resp => {
reloadAll();
setSuccess(true);
}).catch(e => {
Expand Down Expand Up @@ -147,6 +149,11 @@ export const CreateTeam = () => {
<Input autofill={"off"} name={"name"} limit={36} placeholder={t("team_name")} required />
<Input autofill={"off"} name={"password"} placeholder={t("team_secret")} required password />
<SubtleText>{t("team_secret_warn")}</SubtleText>
{groups && groups.data.length &&
<Form.Group htmlFor={"leaderboard_group"} label={"Select your leaderboard group!"}>
<Select options={groups.data.map(i => ({key: i.id, value: i.name}))} name={"leaderboard_group"} />
</Form.Group>
}
</Form.Group>

{message && <Form.Error>{message}</Form.Error>}
Expand Down
97 changes: 97 additions & 0 deletions src/plugins/leaderboardGroups.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (C) 2020-2021 Really Awesome Technology Ltd
//
// This file is part of RACTF.
//
// RACTF is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// RACTF is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with RACTF. If not, see <https://www.gnu.org/licenses/>.

import React, { useContext, useState, useRef } from "react";
import { FiEdit2, FiTrash, FiPlus, FiCrosshair } from "react-icons/fi";

import * as http from "@ractf/util/http";
import { usePaginated } from "@ractf/util/http";
import { ENDPOINTS } from "@ractf/api";
import { registerPlugin } from "@ractf/plugins";
import {
Page, PageHead, Grid, Button, Modal, Input, Form, HR,
UiKitModals, Container, Checkbox
} from "@ractf/ui-kit";


const CMSAdmin = () => {
const [groups, , gRefresh] = usePaginated(ENDPOINTS.LEADERBOARD_GROUPS);
const modals = useContext(UiKitModals);
const [editingGroup, setEditingGroup] = useState(false);
const formSubmit = useRef();

const removeGroup = (group) => {
modals.promptConfirm(<>Are you sure you want to remove <code>{group.name}</code>?</>).then(() => {
http.delete_("/team/groups/" + group.id).then(() => {
gRefresh();
}).catch(() => {
modals.alert("Failed to remove group");
});
}).catch();
};
const editGroup = (group) => {
setEditingGroup(group);
};
const addNew = () => {
setEditingGroup({ name: "", description: "", is_self_assignable: true, has_own_leaderboard: true });
};
const postSubmit = ({ resp }) => {
gRefresh();
setEditingGroup(false);
};

return <Page>
{editingGroup !== false ?
<Modal header={"Editing Group"} onClose={() => setEditingGroup(false)} onConfirm={() => formSubmit.current()}>
<Form postSubmit={postSubmit}
action={(typeof editingGroup.id !== "undefined") ? `/team/groups/${editingGroup.id}/` : "/team/groups/"}
method={(typeof editingGroup.id !== "undefined") ? "PATCH" : "POST"} submitRef={formSubmit}
>
<Form.Group label={"Name"}>
<Input placeholder={"Name"} name={"name"} val={editingGroup.name} required />
</Form.Group>
<Form.Group label={"Description"}>
<Input placeholder={"Description"} name={"description"} val={""} />
</Form.Group>
<Form.Group label={"Self-assignable on team create?"}>
<Checkbox name={"is_self_assignable"} val={editingGroup.is_self_assignable} />
</Form.Group>
<Form.Group label={"Has its own leaderboard page?"}>
<Checkbox name={"has_own_leaderboard"} val={editingGroup.has_own_leaderboard} />
</Form.Group>

<HR />
</Form>
</Modal> : null
}
<PageHead>Leaderboard Groups</PageHead>
<Grid headings={["Name", "Has own leaderboard group", "Self-assignable", "Actions"]} data={[...groups.data.map(i => [
i.name, i.has_own_leaderboard ? "Yes" : "No", i.is_self_assignable ? "Yes" : "No", <Container toolbar>
<Button tiny warning Icon={FiEdit2} onClick={() => editGroup(i)} />
<Button tiny danger Icon={FiTrash} onClick={() => removeGroup(i)} />
</Container>
]), [<Button tiny Icon={FiPlus} onClick={addNew}>Add group</Button>, null, null]]} />
</Page>;
};

export default () => {
registerPlugin("adminPage", "leaderboard_groups", {
component: CMSAdmin,
sidebar: "Leaderboard Groups",
Icon: FiCrosshair,
});
};

0 comments on commit 96608bd

Please sign in to comment.