Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions client/src/components/admin/tables/HidePopup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { postRequest } from "../../../api";
import { Project } from "../../../types";
import { useState } from "react";
import { errorAlert } from "../../../util";
import useAdminStore from "../../../store";

interface HideProjectPopupProps {
/* Projects to edit */
projects: Project[];

/* Function to modify the popup state variable */
close: React.Dispatch<React.SetStateAction<boolean>>;
}

const options = [
{
text: "Lunch",
description: "The team is away for lunch.",
value: "Lunch"
},
{
text: "Low Ranking",
description: "The team is ranked low and is hidden to save judges' time.",
value: "Low Ranking"
},
{
text: "Not Found",
description: "The team has repeatedly not been found for judging.",
value: "Not Found"
},
{
text: "Other",
description: "The team is hidden for another reason (please enter a reason).",
value: "Other"
}
]

const HideProjectPopup = ({ projects, close }: HideProjectPopupProps) => {
const [selectedOption, setSelectedOption] = useState('');
const [selectedReason, setSelectedReason] = useState('');
const fetchProjects = useAdminStore((state) => state.fetchProjects);

const hideProject = async () => {
if (selectedReason == '' || selectedReason == 'Other') {
alert('Please select a reason for hiding the project(s).');
return;
}
const res = await postRequest('/project/hide-many',
{
ids: projects.map(project => project.id),
reason: selectedReason
}
);
if (res.status === 200) {
alert(`Project hidden successfully!`);
} else {
errorAlert(res);
}
await fetchProjects();
close(false);
}
return (
<>
<div className="bg-background fixed z-20 left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] py-6 px-10 w-1/3">
<h1 className="text-5xl font-bold mb-2 text-center">Enter a reason for hiding the project(s)</h1>
<div className="flex flex-row justify-around mt-4">
<ul>
{options.map(({text, description, value}) =>
<div
key={text}
className={`max-w-sm rounded-md p-4 mg-12 gap-4 ${selectedOption == text ? 'bg-lightest' : 'bg-primary/20'}`}
onClick={() => {
setSelectedOption(text);
setSelectedReason(value);
}}
>
<h2>{text}</h2>
<p className="text-sm">{description}</p>
</div>
)}
</ul>
</div>
<div className="bg-background flex flex-row mt-4">
{selectedOption == 'Other' &&
<textarea
className="border-lightest border-2 rounded-md p-2 resize-none w-full"
placeholder="Enter reason here..."
onChange={(e) => { setSelectedReason(e.target.value); }}
/>}
</div>
<div className="flex flex-row justify-around">
<button
className="border-lightest border-2 rounded-full px-6 py-1 mt-4 w-2/5 font-bold text-2xl text-lighter hover:bg-lighter/30 duration-200"
onClick={() => close(false)}
>
Cancel
</button>
<button
className="bg-primary rounded-full px-6 py-1 mt-4 w-2/5 font-bold text-2xl text-background hover:bg-primary/80 duration-200"
onClick={hideProject}
>
Submit
</button>
</div>
</div>
</>
);
};

export default HideProjectPopup;
62 changes: 62 additions & 0 deletions client/src/components/admin/tables/InfoPopup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useState } from 'react';
import { HiddenReason, Project } from '../../../types';
import { convertUnixTimestamp, isActive } from '../../../util';

interface InfoPopupProps {
/* Project to show */
project: Project;

/* Function to modify the popup state variable */
close: React.Dispatch<React.SetStateAction<boolean>>;
}

const renderHiddenReasonRow = (hiddenReason: HiddenReason) => (
<li className="font-mono" key={hiddenReason.when}>{convertUnixTimestamp(hiddenReason.when)}: {hiddenReason.reason}</li>
);

const InfoPopup = ({ project, close }: InfoPopupProps) => {
const [showAll, setShowAll] = useState(false);
return (
<>
<div
className="fixed left-0 top-0 z-20 w-screen h-screen bg-black/30"
onClick={() => close(false)}
></div>
<div className="bg-background fixed z-30 left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] py-6 px-10 w-1/3">
<h1 className="text-5xl font-bold mb-2 text-center">About <span className="text-primary">{project.name}</span></h1>
<p className="text-xl"><b>Description:</b> {project.description}</p>
<p className="text-xl"><b>Guild:</b> {project.guild}</p>
<p className="text-xl"><b>Location:</b> {project.location}</p>
<p className="text-xl"><b>Score:</b> {project.score}</p>
<p className="text-xl"><b>Status:</b> {isActive(project) ? 'Active' : 'Hidden'}</p>
{project.hidden_reasons.length > 0 && (
<div className="mt-4 rounded-lg border border-gray-300 bg-white/80 p-4">
<p className="mb-2 text-xl font-semibold text-gray-800">Hide reasons:</p>
<ul className="m-0 p-0">
{showAll ?
project.hidden_reasons.toReversed().map(hiddenReason => renderHiddenReasonRow(hiddenReason)) :
renderHiddenReasonRow(project.hidden_reasons[project.hidden_reasons.length - 1])
}
</ul>
{project.hidden_reasons.length > 1 && <span
className="mt-2 inline-block cursor-pointer text-pink-500 hover:text-pink-700 transition-colors"
onClick={() => setShowAll(!showAll)}
>
{showAll ? 'Hide' : 'Show more'}
</span>}
</div>
)}
<div className="flex flex-row justify-around">
<button
className=" border-lightest border-2 rounded-full px-6 py-1 mt-4 w-2/5 font-bold text-2xl text-lighter hover:bg-lighter/30 duration-200"
onClick={() => close(false)}
>
Close
</button>
</div>
</div>
</>
);
};

export default InfoPopup;
66 changes: 34 additions & 32 deletions client/src/components/admin/tables/ProjectRow.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {FocusEvent, useEffect, useRef, useState} from 'react';
import {errorAlert, timeSince} from '../../../util';
import {errorAlert, timeSince, isActive} from '../../../util';
import HidePopup from './HidePopup';
import UnhidePopup from './UnhidePopup';
import InfoPopup from './InfoPopup';
import DeletePopup from './DeletePopup';
import EditProjectPopup from './EditProjectPopup';
import useAdminStore from '../../../store';
Expand All @@ -17,6 +20,9 @@ const ProjectRow = ({project, idx, checked, handleCheckedChange}: ProjectRowProp
const [popup, setPopup] = useState(false);
const [editPopup, setEditPopup] = useState(false);
const [deletePopup, setDeletePopup] = useState(false);
const [hidePopup, setHidePopup] = useState(false);
const [unhidePopup, setUnhidePopup] = useState(false);
const [infoPopup, setInfoPopup] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const fetchProjects = useAdminStore((state) => state.fetchProjects);

Expand All @@ -35,15 +41,23 @@ const ProjectRow = ({project, idx, checked, handleCheckedChange}: ProjectRowProp
};
}, [ref]);

const doAction = (action: 'edit' | 'prioritize' | 'hide' | 'delete') => {
switch (action) {
const doAction = async (action: 'edit' | 'prioritize' | 'hide' | 'un-hide' | 'info' | 'delete') => {
switch (action) { // todo: find a way to reuse types for this functon for assignments
case 'edit':
// Open edit popup
setEditPopup(true);
break;
case 'hide':
// Hide
hideProject();
// Open hide popup
setHidePopup(true);
break;
case 'un-hide':
// Un-hide project
setUnhidePopup(true);
break;
case 'info':
// Open info popup
setInfoPopup(true);
break;
case 'delete':
// Open delete popup
Expand All @@ -54,16 +68,6 @@ const ProjectRow = ({project, idx, checked, handleCheckedChange}: ProjectRowProp
setPopup(false);
};

const hideProject = async () => {
const res = await postRequest<YesNoResponse>(project.active ? '/project/hide' : '/project/unhide', {id: project.id});
if (res.status === 200) {
alert(`Project ${project.active ? 'hidden' : 'un-hidden'} successfully!`);
await fetchProjects();
} else {
errorAlert(res);
}
};

const onInputFocusLoss = async (e: FocusEvent<HTMLInputElement>) => {
const res = await postRequest<YesNoResponse>('/project/update-location', {id: project.id, location: e.target.value});
if (res.status === 200) {
Expand All @@ -82,7 +86,7 @@ const ProjectRow = ({project, idx, checked, handleCheckedChange}: ProjectRowProp
'border-t-2 border-backgroundDark duration-150 ' +
(checked
? 'bg-primary/20'
: !project.active
: !isActive(project)
? 'bg-lightest'
: 'bg-background')
}
Expand Down Expand Up @@ -113,25 +117,22 @@ const ProjectRow = ({project, idx, checked, handleCheckedChange}: ProjectRowProp
<td className="text-center">{project.seen}</td>
<td className="text-center">{timeSince(project.last_activity)}</td>
<td className="text-right font-bold flex align-center justify-end">
{popup && (
{popup &&
<div
className="absolute flex flex-col bg-background rounded-md border-lightest border-2 font-normal text-sm"
ref={ref}
>
<div
className="py-1 pl-4 pr-2 cursor-pointer hover:bg-primary/20 duration-150"
onClick={() => doAction('hide')}
>
{project.active ? 'Hide' : 'Un-hide'}
</div>
<div
className="py-1 pl-4 pr-2 cursor-pointer hover:bg-primary/20 duration-150 text-error"
onClick={() => doAction('delete')}
>
Delete
</div>
{['Info', isActive(project) ? 'Hide' : 'Un-hide', 'Delete'].map((action) =>
<div
key={action}
className={`py-1 pl-4 pr-2 cursor-pointer hover:bg-primary/20 duration-150 ${action == 'Delete' ? 'text-error' : ''}`}
onClick={() => { doAction(action.toLowerCase() as 'edit' | 'prioritize' | 'hide' | 'un-hide' | 'info' | 'delete'); }}
>
{action}
</div>
)}
</div>
)}
}
<span
className="cursor-pointer px-1 hover:text-primary duration-150"
onClick={() => {
Expand All @@ -143,9 +144,10 @@ const ProjectRow = ({project, idx, checked, handleCheckedChange}: ProjectRowProp
</td>
</tr>
{deletePopup && <DeletePopup element={project} close={setDeletePopup} />}
{editPopup && <EditProjectPopup project={project} close={setEditPopup} />}
{hidePopup && <HidePopup projects={[project]} close={setHidePopup} />}
{unhidePopup && <UnhidePopup projects={[project]} close={setUnhidePopup}/>}
{infoPopup && <InfoPopup project={project} close={setInfoPopup} />}
</>
);
};

export default ProjectRow;
24 changes: 16 additions & 8 deletions client/src/components/admin/tables/ProjectsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import HeaderEntry from './HeaderEntry';
import { ProjectSortField } from '../../../enums';
import Button from "../../Button";
import {postRequest} from "../../../api";
import {errorAlert} from "../../../util";
import {errorAlert, isActive} from "../../../util";
import {Project, SortField, SortState, YesNoResponse} from "../../../types";
import HidePopup from "./HidePopup";
import UnhidePopup from "./UnhidePopup";

const ProjectsTable = () => {
const unsortedProjects = useAdminStore((state) => state.projects);
Expand All @@ -18,6 +20,8 @@ const ProjectsTable = () => {
field: ProjectSortField.None,
ascending: true,
});
const [hidePopup, setHidePopup] = useState(false);
const [unhidePopup, setUnhidePopup] = useState(false);

const handleCheckedChange = (e: React.ChangeEvent<HTMLInputElement>, i: number) => {
setChecked({ // this change of type is to stop React complaining about "a component is changing an uncontrolled input to be controlled"
Expand Down Expand Up @@ -108,14 +112,16 @@ const ProjectsTable = () => {
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);
if (hide) {
setHidePopup(true);
return;
}
if (projects.filter((_, idx) => checked[idx]).every(project => isActive(project))) {
alert('Selected projects are all active')
return;
}
setUnhidePopup(true);
return;
}

useEffect(() => {
Expand Down Expand Up @@ -242,6 +248,8 @@ const ProjectsTable = () => {
))}
</tbody>
</table>
{hidePopup && <HidePopup projects={projects.filter((_, idx) => checked[idx])} close={setHidePopup} />}
{unhidePopup && <UnhidePopup projects={projects.filter((_, idx) => checked[idx])} close={setUnhidePopup} />}
</div>
);
};
Expand Down
Loading