Skip to content

Commit

Permalink
Merge pull request #20 from ducompsoc/feedback-tweaks
Browse files Browse the repository at this point in the history
Add features and tweaks in response to Quinn and testing requests
  • Loading branch information
tameTNT authored Oct 31, 2024
2 parents bde4a11 + af13f70 commit bc6c51c
Show file tree
Hide file tree
Showing 35 changed files with 403 additions and 157 deletions.
2 changes: 2 additions & 0 deletions client/src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@ const Button = (props: ButtonProps) => {
: 'cursor-auto text-lighter bg-backgroundDark';
const boldFormat = props.bold ? 'font-bold' : 'font-normal';
const widthFormat = props.full ? 'w-full' : 'w-3/4 md:w-2/3';
const focusFormat = 'focus:outline-none focus:ring-inset focus:ring-4 focus:ring-backgroundDark focus:ring-opacity-100';
const formatting = twMerge(
defaultFormat,
borderFormat,
varFormat,
squareFormat,
boldFormat,
widthFormat,
focusFormat,
props.className
);

Expand Down
9 changes: 5 additions & 4 deletions client/src/components/admin/AdminStatsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,21 @@ const AdminStatsPanel = () => {
}, [paused, time]);

return (
<div className="flex flex-row mt-8 w-full">
<div className="flex flex-row mt-8 w-full cursor-pointer" onClick={fetchStats} title="Click to refresh stats">
<PauseButton paused={paused} setPaused={setPaused} clock={time} />
<div className="flex justify-evenly basis-2/5">
<StatBlock name="Projects" value={stats.projects} />
<StatBlock name="Avg Times Seen/Project" value={stats.avg_project_seen} />
<StatBlock name="Hidden Projects" value={stats.hidden_projects} />
<StatBlock name="Avg Seen/Project" value={stats.avg_project_seen} />
</div>
<StatBlock
name="Judging Time"
value={msToTime(time)}
className={'basis-1/5' + (paused ? ' text-error' : '')}
/>
<div className="flex justify-evenly basis-2/5">
<StatBlock name="Avg Projects Seen/Judge" value={stats.avg_judge_seen} />
<StatBlock name="Judges" value={stats.judges} />
<StatBlock name="Avg Projects Seen/Judge" value={stats.avg_judge_seen}/>
<StatBlock name="Judges" value={stats.judges}/>
</div>
</div>
);
Expand Down
3 changes: 1 addition & 2 deletions client/src/components/admin/AdminToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const AdminToolbar = (props: { showProjects: boolean }) => {
bold
full
className="py-2 px-4 rounded-md"
// lucatodo: remove ability to add judges from this admin portal
href='/admin/add-projects'
>
Add Projects
Expand All @@ -38,7 +37,7 @@ const AdminToolbar = (props: { showProjects: boolean }) => {
)}
</div>
<div className="ml-4 italic">
Click on headings to sort by that column.
Click on headings to sort by that column. This will clear your selections.
</div>
{showFlags && <FlagsPopup close={setShowFlags}/>}
</div>
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/admin/FlagsPopup.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { getRequest } from '../../api';
import { errorAlert, timeSince } from '../../util';
import {errorAlert, timeSince, truncate} from '../../util';

interface FlagsPopupProps {
/* Function to modify the popup state variable */
Expand Down Expand Up @@ -98,7 +98,7 @@ const FlagsPopup = ({ close }: FlagsPopupProps) => {
className="flex flex-row items-center text-xl border-b-2 border-backgroundDark py-1"
>
<h2 className="basis-2/5 text-left text-lg text-black">
{`[${flag.project_location}] ${flag.project_name.substring(0, 15)}`}
{`[${flag.project_location}] ${truncate(flag.project_name, 20)}`}
</h2>
<h2 className="basis-1/5 text-left text-lg text-black">
{flag.judge_name}
Expand Down
5 changes: 4 additions & 1 deletion client/src/components/admin/add-projects/NewProjectForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ const NewProjectForm = () => {
<h1 className="text-3xl mb-4">Add Project</h1>
<form className="flex flex-col w-full space-y-4" onSubmit={handleSubmit(onSubmit)}>
<TextInput name="name" placeholder="Name" register={register} />
<TextInput name="location" placeholder="Team location" register={register} />
<div className="flex flex-row w-full mt-4 space-x-6">
<TextInput name="guild" placeholder="Guild" register={register}/>
<TextInput name="location" placeholder="Team location" register={register}/>
</div>
<TextArea
name="description"
placeholder="Description"
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/admin/tables/JudgeRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const JudgeRow = ({ judge, idx, checked, handleCheckedChange }: JudgeRowProps) =
></input>
</td>
<td>{judge.name}</td>
<td className="text-center">{judge.code}</td>
<td className="text-center">{judge.keycloak_user_id}</td>
<td className="text-center">{judge.seen}</td>
<td className="text-center">{judge.past_rankings ? judge.past_rankings.length : 0}</td>
<td className="text-center">{timeSince(judge.last_activity)}</td>
Expand Down
6 changes: 3 additions & 3 deletions client/src/components/admin/tables/JudgesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,13 @@ const JudgesTable = () => {
align="left"
/>
<HeaderEntry
name="Email"
name="Keycloak UserID"
updateSort={updateSort}
sortField={JudgeSortField.Email}
sortField={JudgeSortField.KeycloakUserId}
sortState={sortState}
/>
<HeaderEntry
name="Seen"
name="Projects fully seen (not skipped)"
updateSort={updateSort}
sortField={JudgeSortField.Seen}
sortState={sortState}
Expand Down
5 changes: 3 additions & 2 deletions client/src/components/admin/tables/ProjectRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,18 @@ const ProjectRow = ({project, idx, checked, handleCheckedChange}: ProjectRowProp
<td className="px-2">
<input
type="checkbox"
checked={checked}
checked={checked || false}
onChange={(e) => {
handleCheckedChange(e, idx);
}}
className="cursor-pointer hover:text-primary duration-100"
></input>
</td>
<td className="[&:not(:hover)]:truncate hover:break-words hover:text-wrap">{project.name}</td>
<td className="text-center">{project.guild}</td>
<td className="text-center py-1">
<input
className="w-full md:w-2/3"
className="w-full md:w-2/3 rounded-2xl"
name="location"
key={project.id}
defaultValue={project.location}
Expand Down
209 changes: 160 additions & 49 deletions client/src/components/admin/tables/ProjectsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,23 @@ import ProjectRow from './ProjectRow';
import useAdminStore from '../../../store';
import HeaderEntry from './HeaderEntry';
import { ProjectSortField } from '../../../enums';
import Button from "../../Button";
import {postRequest} from "../../../api";
import {errorAlert} from "../../../util";

const ProjectsTable = () => {
const unsortedProjects = useAdminStore((state) => state.projects);
const fetchProjects = useAdminStore((state) => state.fetchProjects);
const [projects, setProjects] = useState<Project[]>([]);
const [checked, setChecked] = useState<boolean[]>([]);
const [guilds, setGuilds] = useState<string[]>([]);
const [checked, setChecked] = useState<{[key: number]: boolean}>({});
const [sortState, setSortState] = useState<SortState<ProjectSortField>>({
field: ProjectSortField.None,
ascending: true,
});

const handleCheckedChange = (e: React.ChangeEvent<HTMLInputElement>, i: number) => {
setChecked({
setChecked({ // this change of type is to stop React complaining about "a component is changing an uncontrolled input to be controlled"
...checked,
[i]: e.target.checked,
});
Expand Down Expand Up @@ -54,7 +58,14 @@ const ProjectsTable = () => {

// When projects change, update projects and sort
useEffect(() => {
setChecked(Array(unsortedProjects.length).fill(false));
// Reset checked state to an object with all indexes false
setChecked(() => {
let newChecked: {[key: number]: boolean} = {};
unsortedProjects.forEach((_, idx) => {
newChecked[idx] = false;
});
return newChecked;
});

// eslint-disable-next-line @typescript-eslint/no-unused-vars
let sortFunc = (a: Project, b: Project) => 0;
Expand All @@ -63,8 +74,12 @@ const ProjectsTable = () => {
case ProjectSortField.Name:
sortFunc = (a, b) => a.name.localeCompare(b.name) * asc;
break;
case ProjectSortField.Location:
sortFunc = (a, b) => a.location.localeCompare(b.location) * asc;
case ProjectSortField.GuildLocation:
sortFunc = (a, b) => {
const guildComparison = a.guild.localeCompare(b.guild) * asc;
if (guildComparison !== 0) return guildComparison; // if guilds are different, just use those to sort
return a.location.localeCompare(b.location) * asc; // otherwise, secondary sort by location
};
break;
case ProjectSortField.Score:
sortFunc = (a, b) => (a.score - b.score) * asc;
Expand All @@ -80,54 +95,150 @@ const ProjectsTable = () => {
setProjects(unsortedProjects.sort(sortFunc));
}, [unsortedProjects, sortState]);

const bulkHide = async (hide: boolean) => {
let toHide: string[] = [];
Object.entries(checked).forEach(([key, value]) => {
if (value) {
toHide.push(projects[parseInt(key)].id);
}
});

if (toHide.length === 0) {
alert('No projects selected!');
return;
}

const res = await postRequest<YesNoResponse>('/project/hide-unhide-many', {ids: toHide, hide: hide});
if (res.status === 200) {
alert(`${toHide.length} project(s) ${hide ? 'hidden' : 'unhidden'} successfully!`);
await fetchProjects();
} else {
errorAlert(res);
}
}

useEffect(() => {
setGuilds(Array.from(new Set(projects.map(p => p.guild))));
}, [projects]);

const selectByGuild = () => {
const selectedGuild = (document.getElementById('guild-select') as HTMLSelectElement).value;
let toCheck: number[] = [];
if (selectedGuild === 'all') {
toCheck = projects.map((_, idx) => idx);
} else {
projects.forEach((project, idx) => {
if (project.guild === selectedGuild) {
toCheck.push(idx);
}
});
}

setChecked(() => {
let newChecked: {[key: number]: boolean} = {};
toCheck.forEach((idx) => {
newChecked[idx] = true;
});
return newChecked;
});
}

return (
<div className="w-full px-8 pb-4">
<div className="flex flex-row w-full space-x-4 items-center text-center">
<div>
<Button
type="primary"
square
full
className="py-2 px-4 rounded-md"
onClick={() => {
bulkHide(true);
}}
>
Hide Selected
</Button>
</div>
<div>
<Button
type="primary"
square
full
className="py-2 px-4 rounded-md"
onClick={() => {
bulkHide(false);
}}
>
Unhide Selected
</Button>
</div>
<p className="text-2l text-nowrap">{Object.values(checked).filter(Boolean).length} project(s) currently selected</p>
<div className="flex flex-nowrap items-center pl-8">
<p className="text-2xl mr-2 align-middle">Guild:</p>
<select className="rounded-md align-middle" id="guild-select">
{guilds.sort().map((guild, idx) => (
<option key={idx} value={guild}>{guild}</option>
))}
<option key="all-guild" value="all" className="italic">*Select all*</option>
<option key="nil-guild" value="nil" className="italic">*Deselect all*</option>
</select>
<Button type="outline" square className="ml-2 py-2 px-4 rounded-md" onClick={selectByGuild}>
Bulk select
</Button>
</div>
</div>
<table className="table-fixed w-full text-lg">
<tbody>
<tr>
<th className="w-12"></th>
<HeaderEntry
name="Name"
updateSort={updateSort}
sortField={ProjectSortField.Name}
sortState={sortState}
align='left'
/>
<HeaderEntry
name="Location"
updateSort={updateSort}
sortField={ProjectSortField.Location}
sortState={sortState}
/>
<HeaderEntry
name="Live Score"
updateSort={updateSort}
sortField={ProjectSortField.Score}
sortState={sortState}
/>
<HeaderEntry
name="Seen"
updateSort={updateSort}
sortField={ProjectSortField.Seen}
sortState={sortState}
/>
<HeaderEntry
name="Updated"
updateSort={updateSort}
sortField={ProjectSortField.Updated}
sortState={sortState}
/>
<th className="text-right w-24">Actions</th>
</tr>
{projects.map((project: Project, idx) => (
<ProjectRow
key={idx}
idx={idx}
project={project}
checked={checked[idx]}
handleCheckedChange={handleCheckedChange}
/>
))}
<tr>
<th className="w-12"></th>
<HeaderEntry
name="Name"
updateSort={updateSort}
sortField={ProjectSortField.Name}
sortState={sortState}
align='left'
/>
<HeaderEntry
name="Guild"
updateSort={updateSort}
sortField={ProjectSortField.GuildLocation}
sortState={sortState}
/>
<HeaderEntry
name="Location"
updateSort={updateSort}
sortField={ProjectSortField.GuildLocation}
sortState={sortState}
/>
<HeaderEntry
name="Live Score"
updateSort={updateSort}
sortField={ProjectSortField.Score}
sortState={sortState}
/>
<HeaderEntry
name="Seen (and not skipped)"
updateSort={updateSort}
sortField={ProjectSortField.Seen}
sortState={sortState}
/>
<HeaderEntry
name="Updated"
updateSort={updateSort}
sortField={ProjectSortField.Updated}
sortState={sortState}
/>
<th className="text-right w-24">Actions</th>
</tr>
{projects.map((project: Project, idx) => (
<ProjectRow
key={idx}
idx={idx}
project={project}
checked={checked[idx]}
handleCheckedChange={handleCheckedChange}
/>
))}
</tbody>
</table>
</div>
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/judge/ProjectDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const ProjectDisplay = (props: ProjectDisplayProps) => {
{project.name}
</a>
</h1>
<h2 className="text-xl mb-1">Location (Guild|Table): {project.location}</h2>
<h2 className="text-xl mb-1">Location: {project.guild}|{project.location}</h2>
<Paragraph className="text-light" text={project.description} />
</div>
);
Expand Down
Loading

0 comments on commit bc6c51c

Please sign in to comment.