Skip to content
Merged
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
51 changes: 43 additions & 8 deletions src/components/moderation/ViewChallenge.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Dialog, Transition } from '@headlessui/react';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { useEffect, useState } from 'react';
import request from '@/utils/request';
import { MarkdownViewer } from '../MarkdownViewer';

const ViewChallenge = ({ open, setOpen, selected }) => {

Expand Down Expand Up @@ -50,7 +51,7 @@ const ViewChallenge = ({ open, setOpen, selected }) => {
leaveTo="translate-x-full"
>

<Dialog.Panel className="pointer-events-auto w-screen max-w-xl">
<Dialog.Panel className="pointer-events-auto w-screen max-w-6xl">
<div className="flex h-full flex-col divide-y divide-neutral-800 bg-neutral-900 border-l-2 border-neutral-800 shadow-xl">
<div className="flex min-h-0 flex-1 flex-col overflow-y-scroll py-6">
<div className="px-4 sm:px-6">
Expand All @@ -73,15 +74,21 @@ const ViewChallenge = ({ open, setOpen, selected }) => {
</div>
<div className=" mt-6 px-4 sm:px-6 text-white">

<div className='bg-neutral-800 p-3 gap-x-4 w-full'>

<p> <i className="fas fa-exclamation-triangle"></i> Don't forget to make sure the challenge is including an explanation.</p>

</div>
<br></br>
<div className='grid grid-cols-3 gap-y-8'>
<div className='col-span-3'>
<h1 className='font-bold text-blue-600 mb-1'>Description</h1>
<p>{challenge && challenge.content}</p>

<MarkdownViewer className='bg-neutral-800 p-3 rounded-md' content={challenge && challenge.content} />
</div>
<div>
<h1 className='font-bold text-blue-600 mb-1'>Flag</h1>
<p>{challenge && challenge.solution || "N/A"}</p>
<p>{challenge && challenge.solution && challenge.solution.keyword || "N/A"}</p>
</div>
<div>
<h1 className='font-bold text-blue-600 mb-1'>Difficulty</h1>
Expand Down Expand Up @@ -120,7 +127,16 @@ const ViewChallenge = ({ open, setOpen, selected }) => {
</textarea>

<br></br><br></br>
<h1 className='font-bold text-blue-600 mb-1'>Set Bonus Points</h1>
<div className='flex rounded-md'>
<div> <h1 className='font-bold text-blue-600 mb-1'>Set Base Points</h1>
</div>
<div className='ml-auto'><p className='mb-2 text-sm font-bold'><i className="fas fa-info-circle"></i> <span className='text-blue-200'>Beginner: 100</span>,
<span className='text-green-200'> Easy: 200</span>,
<span className='text-yellow-200'> Medium: 300</span>,
<span className='text-red-200'> Hard: 400</span>,
<span className='text-indigo-200'> Insane: 500</span>
</p></div>
</div>
<input type="number" placeholder="0" className='bg-neutral-800 w-full border-neutral-700' />

</div>
Expand All @@ -135,11 +151,22 @@ const ViewChallenge = ({ open, setOpen, selected }) => {
className="rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm ring-1 ring-inset ring-gray-300 hover:ring-gray-400"
onClick={() => setOpen(false)}
>
<XMarkIcon className="h-5 w-5 inline mr-2" aria-hidden="true" />
Cancel
</button>
<button className='ml-4 rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500' onClick={() => window.open(`../../challenges/${selected}`, '_blank')}>
<i className="fas fa-external-link-alt mr-2"></i>
Go to Challenge Page
</button>

<button className='ml-4 w-auto rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500' onClick={() => window.open(`../../create/edit?id=${selected}`, '_blank')}>
<i className="fas fa-edit mr-2"></i>
Edit Challenge
</button>

<button
onClick={async () => {
const reason = prompt("Please enter the reason for denial:");
const reason = document.querySelector('textarea').value;
if (reason) {
try {
await request(`${process.env.NEXT_PUBLIC_API_URL}/challenges/${selected}/deny`, "POST", { reason });
Expand All @@ -148,11 +175,14 @@ const ViewChallenge = ({ open, setOpen, selected }) => {
console.error(error);
alert("Failed to deny challenge!");
}
} else {
alert("Please provide a reason in the moderator notes.");
}
}}
type="button"
className="ml-4 inline-flex justify-center rounded-md bg-yellow-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-yellow-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-yellow-500"
className="ml-4 justify-center rounded-md bg-yellow-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-yellow-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-yellow-500"
>
<i className="fas fa-exclamation-triangle mr-2"></i>
Request Changes
</button>
<button
Expand All @@ -162,22 +192,27 @@ const ViewChallenge = ({ open, setOpen, selected }) => {
console.log(basePoints)
try {
await request(`${process.env.NEXT_PUBLIC_API_URL}/challenges/${selected}/approve`, "POST", { basePoints: basePoints });
//reload
window.location.reload();
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
}}
type="submit"
className="ml-4 inline-flex justify-center rounded-md bg-green-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-500"
className="ml-4 justify-center rounded-md bg-green-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-500"
>
{isLoading ? (
<svg className="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
) : (
"Approve"
<>
<i className="fas fa-check mr-2"></i>
Approve
</>
)}
</button>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/create/edit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ export default function Createchall() {
// setPenalty(challenge.penalty);
setContentPreview(challenge.content);
setSolution(challenge.solution);
setDifficulty(challenge.difficulty);
setDifficulty(challenge.difficulty.toLowerCase());
setNewConfig(challenge.commands.replace(/ && /g, '\n'));
}
} catch (error) {
Expand Down
12 changes: 10 additions & 2 deletions src/pages/create/new.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ export default function Createchall() {
category,
commands: nConfig,
fileId: fileId,
isRoot: document.getElementById('root').checked,
};

const url = `${process.env.NEXT_PUBLIC_API_URL}/challenges/create`;
Expand Down Expand Up @@ -625,7 +626,7 @@ export default function Createchall() {
</div>
</div>

<div className='grid grid-cols-2 mt-5 gap-x-1'>
<div className='grid grid-cols-1 mt-5 gap-x-1'>
<div className="900 rounded-sm bg-neutral-800/40 shadow-lg ">
<h3 className="m flex items-center bg-blue-800 px-4 py-4 text-xl font-medium leading-6 text-white flex">
<FontAwesomeIcon icon={faServer} className='mr-2 text-sm w-4 h-4' />
Expand Down Expand Up @@ -721,9 +722,16 @@ export default function Createchall() {
Please assume that files are placed in the home directory.
</h1>
</div>

<div>
<input id='root' type='checkbox' defaultChecked={true} />
<label className='ml-2 '>Make user login have root access. By default, we suggest giving users root.</label>
</div>

<br></br>
</div>
</div>
<div className="900 rounded-sm bg-neutral-800/40 shadow-lg ">
<div className="900 rounded-sm bg-neutral-800/40 shadow-lg hidden ">
<h3 className="m flex items-center bg-blue-800 px-4 py-4 text-xl font-medium leading-6 text-white flex">
<FontAwesomeIcon icon={faGlobe} className='mr-2 text-sm w-4 h-4' />
Hosted Web Challenges
Expand Down
108 changes: 101 additions & 7 deletions src/pages/moderation.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,31 @@ export default function Competitions() {
}
};

const deleteBulk = async () => {
if (selectedChallenges.length === 0) {
alert("No challenges selected.");
return;
}

if (!confirm("Are you sure you want to delete the selected challenges?")) {
return;
}
try {
const response = await request(`${process.env.NEXT_PUBLIC_API_URL}/admin/deleteChallenges`, "POST", { challengeIds: selectedChallenges });
if (response.success) {
alert("Selected challenges deleted successfully!");
setSelectedChallenges([]); // Clear selected challenges
fetchPendingChallenges(); // Refresh pending challenges
} else {
alert("Failed to delete selected challenges.");
}
} catch (error) {
console.error(error);
alert("An error occurred while deleting the selected challenges.");
}
};


const handleResetBanner = async () => {
const username = document.getElementById('usernameInput').value;
const reason = document.getElementById('reasonInput').value;
Expand Down Expand Up @@ -280,6 +305,21 @@ export default function Competitions() {
}
};

// Fetch platform stats
const [stats, setStats] = useState(null);

useEffect(() => {
const fetchStats = async () => {
try {
const response = await request(`${process.env.NEXT_PUBLIC_API_URL}/admin/stats`, "GET");
setStats(response);
} catch (error) {
console.error("Failed to fetch platform stats", error);
}
};
fetchStats();
}, []);

return (
<>
<Head>
Expand Down Expand Up @@ -310,16 +350,31 @@ export default function Competitions() {
<div className='grid grid-cols-2 gap-x-5 border border-neutral-700 px-4 py-4'>

<div>

<h1 className='text-xl text-white mb-2'>USER ACTIONS</h1>
<input type="text" placeholder='Enter USERNAME' className='mb-2 text-white bg-neutral-800 border-none w-full' id="usernameInput"></input>
<textarea placeholder='Reason' className='mb-2 text-white bg-neutral-800 border-none w-full' id="reasonInput"></textarea>
<button className='ml-auto px-2 py-1 bg-red-600 text-white mt-2'onClick={handleDisableAccount}>Disable Account</button>
<button className='ml-2 px-2 py-1 bg-green-600 text-white mt-2'onClick={handleEnableAccount}>Enable Account</button>
<button className='ml-2 px-2 py-1 bg-yellow-600 text-white mt-2' onClick={handleWarnUser}>Warn User</button>
<button className='ml-2 px-2 py-1 bg-blue-600 text-white mt-2' onClick={handleResetPFP}>Reset PFP</button>
<button className='ml-2 px-2 py-1 bg-blue-600 text-white mt-2' onClick={handleResetBanner}>Reset Banner</button>
<button className='ml-2 px-2 py-1 bg-blue-600 text-white mt-2' onClick={handleResetBio}>Reset Bio</button>

<div className='grid grid-cols-3 gap-x-2'>
<button className=' px-2 py-1 bg-red-600 hover:bg-red-500 duration-300 text-white mt-2'onClick={handleDisableAccount}>
<i className='fa fa-ban mr-2'></i>Disable Account
</button>
<button className=' px-2 py-1 bg-green-600 hover:bg-green-500 duration-300 text-white mt-2'onClick={handleEnableAccount}>
<i className='fa fa-check mr-2'></i>Enable Account
</button>
<button className=' px-2 py-1 bg-yellow-600 hover:bg-yellow-500 duration-300 text-white mt-2' onClick={handleWarnUser}>
<i className='fa fa-exclamation-triangle mr-2'></i>Warn User
</button>
<button className=' px-2 py-1 bg-blue-600 hover:bg-blue-500 duration-300 text-white mt-2' onClick={handleResetPFP}>
<i className='fa fa-user-circle mr-2'></i>Reset PFP
</button>
<button className='px-2 py-1 bg-blue-600 hover:bg-blue-500 duration-300 text-white mt-2' onClick={handleResetBanner}>
<i className='fa fa-image mr-2'></i>Reset Banner
</button>
<button className=' px-2 py-1 bg-blue-600 hover:bg-blue-500 duration-300 text-white mt-2' onClick={handleResetBio}>
<i className='fa fa-info-circle mr-2'></i>Reset Bio
</button>

</div>

</div>

Expand All @@ -336,11 +391,13 @@ export default function Competitions() {
<div className='grid grid-cols-2 mt-4 gap-x-5 border border-neutral-700 px-4 py-4'>
<div className='text-white text-xl'>
<h1>Challenges Pending Approval</h1>
{selectedChallenges.length > 0 && <button className='ml-auto px-2 py-1 bg-red-600 text-sm text-white mt-2' onClick={deleteBulk}><i className='fa fa-trash mr-2'></i>Delete Selected</button>}
<div className='mt-2'>
{pendingChallenges.length > 0 ? (
pendingChallenges.map((challenge) => (
<div key={challenge.id} onClick={() => {setSelectedId(challenge.id); setChallengeIsOpen(true);}} className='bg-neutral-800 w-full mb-2 border focus:bg-blue-900 focus:border-blue-500 border-neutral-700 hover:bg-neutral-700/50 cursor-pointer px-2 py-1 flex items-center text-sm'>
<input
onClick={(e) => e.stopPropagation()} // Prevent click event propagation
type="checkbox"
checked={selectedChallenges.includes(challenge.id)}
onChange={() => handleSelectChallenge(challenge.id)}
Expand Down Expand Up @@ -383,6 +440,43 @@ export default function Competitions() {
</div>
</div>

<div className='mt-4 grid gap-x-5 border border-neutral-700 px-4 py-4'>
<h1 className='text-xl text-white mb-2'>Platform Stats</h1>
{stats ? (
<div className='text-white'>
<div className='grid grid-cols-3 gap-4'>
<div className='bg-neutral-800 p-4 rounded-lg text-center'>
<p className='text-white text-xl font-bold'>{stats.userCount}</p>
<p className='text-gray-400'>Total Users</p>
</div>
<div className='bg-neutral-800 p-4 rounded-lg text-center'>
<p className='text-white text-xl font-bold'>{stats.challengeCount}</p>
<p className='text-gray-400'>Total Challenges</p>
</div>
<div className='bg-neutral-800 p-4 rounded-lg text-center'>
<p className='text-white text-xl font-bold'>{stats.verifiedChallengeCount}</p>
<p className='text-gray-400'>Verified Challenges</p>
</div>

</div>

<h2 className='mt-4 text-white text-lg '>Recent Sign-Ups</h2>
<div className='mt-2 grid grid-cols-3 gap-4'>
{stats.recentSignUps.map((user) => (
<div key={user.id} className='hover:bg-neutral-700 duration-100 bg-neutral-800 cursor-pointer p-4 rounded-lg flex items-center' onClick={() => window.open(`/users/${user.username}`, '_blank')}>
<img src={user.profileImage || 'https://robohash.org/' + user.username} alt={`${user.username}'s profile`} className='w-12 h-12 rounded-full mr-4' />
<div>
<p className='text-white font-bold'>{user.username}</p>
<p className='text-gray-400 text-sm'>{new Date(user.createdAt).toLocaleString()}</p>
</div>
</div>
))}
</div>
</div>
) : (
<p className='text-red-500'>Failed to load stats</p>
)}
</div>
<h1 className='mt-20 text-white hidden'>Selected Content ID's</h1>
<textarea value={selectedChallenges.join(", ") || "N/A"} className='hidden text-white bg-neutral-800 border-none w-full'>
</textarea>
Expand Down