diff --git a/.env.development b/.env.development index c9f5e9b9..c92c4be3 100644 --- a/.env.development +++ b/.env.development @@ -10,9 +10,12 @@ #NEXT_PUBLIC_APP_MEASUREMENT_ID=G-7ZNKM9VFN2 # TDO +# +NEXT_PUBLIC_API_URL=https://node-api-vchgui6lfq-uc.a.run.app +NEXT_PUBLIC_FRONTEND_URL=https://preview.ctfguide.com +#NEXT_PUBLIC_API_URL=http://localhost:3001 +#NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000 -NEXT_PUBLIC_API_URL=http://localhost:3001 -NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000 NEXT_PUBLIC_APP_API_KEY=AIzaSyAHz1s-UuNhlZ6aKvqwzmzzidzWxBKw9hw NEXT_PUBLIC_APP_AUTH_DOMAIN=ctfguide-dev.firebaseapp.com NEXT_PUBLIC_APP_PROJECT_ID=ctfguide-dev @@ -23,3 +26,4 @@ NEXT_PUBLIC_APP_MEASUREMENT_ID=G-7ZNKM9VFN2 NEXT_PUBLIC_TERM_URL=https://file-system-run-qi6ms4rtoa-ue.a.run.app/ NEXT_PUBLIC_APP_STRIPE_KEY=pk_test_51NyMUrJJ9Dbjmm7hji7JsdifB3sWmgPKQhfRsG7pEPjvwyYe0huU1vLeOwbUe5j5dmPWkS0EqB6euANw2yJ2yQn000lHnTXis7 NEXT_PUBLIC_KANA_SERVER_URL=kana-server.ctfguide.com +NEXT_PUBLIC_GOOGLE_CLIENT_ID=166652277588-4uk7g7irqlicacelg1nfgt0ejmskmo9h.apps.googleusercontent.com diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e69de29b diff --git a/components.json b/components.json new file mode 100644 index 00000000..68f74b09 --- /dev/null +++ b/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": false, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/styles/globals.css", + "baseColor": "gray", + "cssVariables": false + }, + "aliases": { + "utils": "@/lib/utils", + "components": "@/components" + } +} \ No newline at end of file diff --git a/next.config.js b/next.config.js index 79dbc16e..9864f0fb 100644 --- a/next.config.js +++ b/next.config.js @@ -1,3 +1,5 @@ +const removeImports = require("next-remove-imports")(); + /** @type {import('next').NextConfig} */ const nextConfig = { webpack: (config, { dev }) => { @@ -15,8 +17,10 @@ const nextConfig = { }, experimental: { scrollRestoration: true, + esmExternals: true, // Add this to ensure esmExternals is true }, - ignoreDuringBuilds: true -} + ignoreDuringBuilds: true, + transpilePackages: ['react-md-editor'] +}; -module.exports = nextConfig +module.exports = removeImports(nextConfig); diff --git a/package.json b/package.json index 2cd76e58..93bd8033 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,12 @@ "@heroicons/react": "^2.0.13", "@mui/material": "^5.15.2", "@mui/x-charts": "^6.18.4", + "@react-oauth/google": "^0.12.1", "@stripe/stripe-js": "^2.1.7", "@tailwindcss/forms": "^0.5.3", "@tremor/react": "^1.8.1", + "@uiw/react-markdown-editor": "^6.1.1", + "@uiw/react-md-editor": "3.6.0", "asciinema-player": "3.6.3", "autoprefixer": "^10.4.12", "babel-plugin-macros": "^3.1.0", @@ -33,19 +36,19 @@ "dotenv": "^16.0.3", "easymde": "^2.18.0", "enable": "^3.4.0", - "firebase": "^9.16.0", "focus-visible": "^5.2.0", "framer-motion": "^10.2.4", "heroicons": "^2.0.13", + "jwt-decode": "^4.0.0", "marked": "^4.3.0", "material-ui-popup-state": "^5.0.5", "next": "^14.0.2", + "next-remove-imports": "^1.0.12", "postcss-focus-visible": "^6.0.4", "puppeteer": "^22.0.0", "react": "18.2.0", "react-chartjs-2": "^5.2.0", "react-collapsible": "^2.10.0", - "react-countup": "^2.4.0", "react-dom": "18.2.0", "react-firebase-hooks": "^5.1.1", "react-google-charts": "^4.0.1", @@ -53,19 +56,23 @@ "react-loading-skeleton": "^3.2.0", "react-markdown": "^8.0.7", "react-router-dom": "^6.9.0", + "react-select-country-list": "^2.2.3", "react-simplemde-editor": "^5.2.0", "react-text-loop": "^2.3.0", "react-text-transition": "^3.1.0", "react-toastify": "^10.0.5", "react-tooltip": "^5.25.1", "react-top-loading-bar": "^2.3.1", + "react-transition-group": "^4.4.5", "react-visibility-sensor": "^5.1.1", "reactjs-popup": "^2.0.5", "recharts": "^2.5.0", "simplemde": "^1.11.2", "socket.io-client": "^4.7.4", "stripe": "^13.9.0", + "tailwind-merge": "^2.3.0", "tailwindcss": "^3.2.1", + "tailwindcss-animate": "^1.0.7", "xterm": "^5.3.0" }, "devDependencies": { diff --git a/public/flag.png b/public/flag.png new file mode 100644 index 00000000..c13249e1 Binary files /dev/null and b/public/flag.png differ diff --git a/public/icons8-leaderboard.svg b/public/icons8-leaderboard.svg new file mode 100644 index 00000000..1f2c14b3 --- /dev/null +++ b/public/icons8-leaderboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/newflag.svg b/public/newflag.svg new file mode 100644 index 00000000..90a2a6a3 --- /dev/null +++ b/public/newflag.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/site.png b/public/site.png new file mode 100644 index 00000000..66e6ae54 Binary files /dev/null and b/public/site.png differ diff --git a/src/components/AuthPopup.jsx b/src/components/AuthPopup.jsx index e01ee6a2..9ca1c2a7 100644 --- a/src/components/AuthPopup.jsx +++ b/src/components/AuthPopup.jsx @@ -1,24 +1,10 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; -import { getAuth, onAuthStateChanged } from 'firebase/auth'; +//import { getAuth, onAuthStateChanged } from 'firebase/auth'; export function AuthPopup() { // check if firebase logged in const [user, setUser] = useState(false); - const auth = getAuth(); - useEffect(() => { - const auth = getAuth(); - onAuthStateChanged(auth, (user) => { - if (user) { - // User is signed in, see docs for a list of available properties - // https://firebase.google.com/docs/reference/js/firebase.User - const uid = user.uid; - setUser(false); - } else { - setUser(false); - } - }); - }); if (user) { return
{/*User logged in*/}
; } else { diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 47ed4dcd..94c77e89 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -112,4 +112,4 @@ export function Header() { ); -} +} \ No newline at end of file diff --git a/src/components/Logo.jsx b/src/components/Logo.jsx index 30cdb7f5..72ebdb47 100644 --- a/src/components/Logo.jsx +++ b/src/components/Logo.jsx @@ -1,12 +1,12 @@ export function Logo(props) { return (
- +

- CTFGuide BETA + CTFGuide

); diff --git a/src/components/MarkdownViewer.js b/src/components/MarkdownViewer.js index 2095b530..dd363e47 100644 --- a/src/components/MarkdownViewer.js +++ b/src/components/MarkdownViewer.js @@ -1,9 +1,9 @@ import React from 'react'; import ReactMarkdown from 'react-markdown'; -export function MarkdownViewer({content, className = ""}) { +export function MarkdownViewer({ content, className = "" }) { return ( -
+
{content}
); diff --git a/src/components/PersonCard.jsx b/src/components/PersonCard.jsx index e7b34d29..3122510e 100644 --- a/src/components/PersonCard.jsx +++ b/src/components/PersonCard.jsx @@ -9,7 +9,7 @@ export default function PersonCard({ person }) { return ( -

{person.personName}

-

{person.position}

-
+

{person.personName}

+

{person.position}

+
{ - window.location.replace('/login'); - }) - .catch((error) => { - console.log(error); - }); + document.cookie = 'idToken=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + router.push('/login'); } - - // if user signed out redirect - - // if env is NEXT_PUBLIC_APP_AUTH_DOMAIN=ctfguide-dev.firebaseapp.com the logo should say CTFGuide Developer not CTFGuide Beta - - - + useEffect(() => { if (!localStorage.getItem('dismissStatus')) { setShowBanner(true); } - - - if ( - process.env.NEXT_PUBLIC_APP_AUTH_DOMAIN === 'ctfguide-dev.firebaseapp.com' - ) { - // setIsAdmin(true) - } + }, []); + // Function to close the modal + const closeModal = () => { + setShowSearchModal(false); + }; + + // Effect to add and remove the event listener + useEffect(() => { + const handleKeyDown = (event) => { + if (event.keyCode === 27) { // 27 is the key code for ESC key + closeModal(); + } + }; + + // Add event listener + document.addEventListener('keydown', handleKeyDown); + + // Remove event listener on cleanup + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, []); + + const toggleSearchModal = () => { + setShowSearchModal(prev => !prev); // Toggle the state + }; const [notification, showNotifications] = useState(false); const [notificationData, setNotificationData] = useState([ @@ -89,32 +115,28 @@ export function StandardNav(props) { return; } - - if (localStorage.getItem("pfp")) { - setPfp(localStorage.getItem("pfp")); - } - - + if (localStorage.getItem("pfp")) { + setPfp(localStorage.getItem("pfp")); + } - - const fetchData = async () => { - try { - const endPoint = process.env.NEXT_PUBLIC_API_URL + '/users/' + username + '/pfp'; - const result = await request(endPoint, "GET", null); - if (result) { - setPfp(result) - } else { - setPfp(`https://robohash.org/${username}.png?set=set1&size=150x150`) - } - - } catch (err) { - console.log('failed to get profile picture') - } - }; - fetchData(); + const fetchData = async () => { + try { + const endPoint = process.env.NEXT_PUBLIC_API_URL + '/users/' + username + '/pfp'; + const result = await request(endPoint, "GET", null); + if (result) { + setPfp(result) + } else { + setPfp(`https://robohash.org/${username}.png?set=set1&size=150x150`) + } + + } catch (err) { + console.log('failed to get profile picture') + } + }; + fetchData(); }, [username]); - useEffect(() => { + useEffect(() => { const fetchNotification = async () => { const endPoint = process.env.NEXT_PUBLIC_API_URL + '/account/notifications'; const result = await request(endPoint, 'GET', null); @@ -149,7 +171,7 @@ export function StandardNav(props) { receivedTime: noti + ' ago', detailPage: '/events', image: - 'https://cutshort-data.s3.amazonaws.com/cloudfront/public/companies/5809d1d8af3059ed5b346ed1/logo-1615367026425-logo-v6.png', + 'https://cutshort-data.s3.amazonaws.com/cloudfront/public/companies/5809d1d8af3059ed5b346ed1/logo-1615367026425-logo-v6.png', }; }) ); @@ -158,7 +180,6 @@ export function StandardNav(props) { fetchNotification(); }, []); - function dismissStatus() { localStorage.setItem('dismissStatus', true); setShowBanner(false); @@ -169,24 +190,45 @@ export function StandardNav(props) { const url = `${process.env.NEXT_PUBLIC_API_URL}/notification`; const data = await request(url, 'GET', null); console.log(data); - if(data.success) { + if (data.success) { setNotifications(data.body); console.log(data); } else { setNotifications(["Unable to get notification, try again"]); } - } catch(err) { + } catch (err) { console.log(err); } } + const linkClass = (path) => `inline-flex items-center border-b-2 px-4 pt-1 text-md font-semibold transition-all ${ + router.pathname === path ? 'text-blue-500 border-blue-500' : 'text-gray-300 hover:text-gray-50 border-transparent' + }`; + return ( <> - + {isPopoverOpen && ( +
setShowSearchModal(false)} + > +

test

+ +

+
+ )} + + + + + {({ open }) => ( <> -
+
@@ -207,206 +249,262 @@ export function StandardNav(props) {
- - {isAdmin ? : } + +
-
- {/* Current: "border-blue-500 text-white", Default: "border-transparent text-gray-300 hover:font-bold" */} +
+ - Dashboard - - - Learn + Practice - Classes + Competitions - Practice + Leaderboards Create - + - Live + Classrooms - - EDU - - {isAdmin && ( -

- CTFGUIDE INTERNAL -

- )} + {/* Ellipsis dropdown */} + + {({ open }) => ( + <> + + + + +
+
+ +

+ Create +

+ + +

+ Classrooms +

+ +
+
+
+
+ + )} +
+ + {/*search bar*/} +
+
setShowSearchModal(true)} + > + + Search for anything +
+
+ {/* */} + {/* Live */} + {/* */} + {/**/} + {/* */} + {/* EDU */} + {/* */} +
- { !guestAllowed && + {!guestAllowed &&
-
-

- {points} -

-
-
-

- 0 -

-
- + + + +
-
} - +
- {/* Current: "bg-blue-50 border-blue-500 text-blue-700", Default: "border-transparent text-gray-300 hover:font-bold" */} + {/* Current: "bg-blue-50 border-blue-500 text-blue-700", Default: "border-transparent text-gray-300 hover:text-white" */} - Your Profile + Profile Settings + + Sign out @@ -472,19 +571,26 @@ export function StandardNav(props) { )} - - - {isAdmin && ( -
-

CTFGuide is running in development mode.

-
- )} - - {!['/groups', '/assignments', '/submissions'].some(path => router.pathname.includes(path) || !showBanner) && ( -
-

Limited feature availability for GP. View entire site status here. Dismiss

-
- )} + + + + + + + + + + + { + !['/groups', '/assignments', '/submissions'].some(path => router.pathname.includes(path) || !showBanner) && ( +
+

Limited feature availability for GP. View entire site status here. Dismiss

+
+ ) + } + + + ); } diff --git a/src/components/Table.jsx b/src/components/Table.jsx new file mode 100644 index 00000000..671e82f7 --- /dev/null +++ b/src/components/Table.jsx @@ -0,0 +1,122 @@ + +import { TableHead, TableRow, TableHeader, TableCell, TableBody, Table } from "@/components/ui/table" +import Link from "next/link" + +export function MyTable() { + return ( + (
+ + + + Competition + Date + Location + Website + + + + + +
+ + + DefCon CTF +
+
+ August 11-13, 2024 + Las Vegas, NV + + + View Website + + +
+ + +
+ + UCSB iCTF +
+
+ October 6-8, 2024 + Santa Barbara, CA + + + View Website + + +
+ + +
+ + CSAW CTF +
+
+ November 10-12, 2024 + New York, NY + + + View Website + + +
+ + +
+ + CTFGuide Fest 2024 +
+
+ Year-round + Online + + + View Website + + +
+ + +
+ + SANS Holiday Hack Challenge +
+
+ December 1-31, 2024 + Online + + + View Website + + +
+
+
+
) + ); +} + +function TrophyIcon(props) { + return ( + ( + + + + + + + ) + ); +} diff --git a/src/components/dashboard/DashboardHeader.jsx b/src/components/dashboard/DashboardHeader.jsx index 2a59ad95..c4f08e89 100644 --- a/src/components/dashboard/DashboardHeader.jsx +++ b/src/components/dashboard/DashboardHeader.jsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; import Skeleton from 'react-loading-skeleton'; import 'react-loading-skeleton/dist/skeleton.css'; import request from '@/utils/request'; +import Link from 'next/link'; export function DashboardHeader() { const [username, setUsername] = useState(null); @@ -19,12 +20,12 @@ export function DashboardHeader() { useEffect(() => { try { - request(`${process.env.NEXT_PUBLIC_API_URL}/account`, 'GET', null) + request(`${process.env.NEXT_PUBLIC_API_URL}/account`, 'GET', null) .then((data) => { setUsername(data.username); setLocation(data.location); localStorage.setItem("username", data.username) - + setJoin(data.createdAt.substring(0, 10)); if (data.githubUrl) { setGithub(`https://github.com/${data.githubUrl}`); @@ -41,28 +42,28 @@ export function DashboardHeader() { }, []); // get user's profile picture - useEffect(() => { - if (!username) { - return; - } - const fetchData = async () => { - try { - const endPoint = process.env.NEXT_PUBLIC_API_URL + '/users/' + username + '/pfp'; - const result = await request(endPoint, "GET", null); - console.log(result) - if (result) { - setPfp(result) - localStorage("pfp", result) - } else { - setPfp(`https://robohash.org/${username}.png?set=set1&size=150x150`) - } + useEffect(() => { + if (!username) { + return; + } + const fetchData = async () => { + try { + const endPoint = process.env.NEXT_PUBLIC_API_URL + '/users/' + username + '/pfp'; + const result = await request(endPoint, "GET", null); + console.log(result) + if (result) { + setPfp(result) + localStorage("pfp", result) + } else { + setPfp(`https://robohash.org/${username}.png?set=set1&size=150x150`) + } - } catch (err) { - console.log('failed to get profile picture') - } - }; - fetchData(); - }, [username]); + } catch (err) { + console.log('failed to get profile picture') + } + }; + fetchData(); + }, [username]); function createPopupWin(pageURL, pageTitle, popupWinWidth, popupWinHeight) { @@ -87,10 +88,9 @@ export function DashboardHeader() {

{username || ( - + )}

- {' '} + {' '} {location || ( - + )}

@@ -144,19 +144,15 @@ export function DashboardHeader() { )}
-
{ - createPopupWin('../terminal', 'CTFGuide Terminal', 1200, 650); - - }} className="hidden cursor-pointer ml-2 mt-8 mb-0 rounded-lg px-10 py-1 flex items-center space-x-1 duration-4000 bg-neutral-800 transition ease-in-out hover:bg-neutral-800/40" > -

Terminal

+

Terminal

- Submit Report! + Submit Report
diff --git a/src/components/dashboard/Suggest.jsx b/src/components/dashboard/Suggest.jsx index 735fdb21..7ce080ec 100644 --- a/src/components/dashboard/Suggest.jsx +++ b/src/components/dashboard/Suggest.jsx @@ -1,3 +1,5 @@ +import Link from "next/link"; + export function Suggest() { return ( <> @@ -10,22 +12,10 @@ export function Suggest() {
- +
- - - - ); diff --git a/src/components/design/CardDecorator.jsx b/src/components/design/CardDecorator.jsx new file mode 100644 index 00000000..275f1332 --- /dev/null +++ b/src/components/design/CardDecorator.jsx @@ -0,0 +1,17 @@ +/** + * @param props {import("react").HTMLAttributes & {position?: 'top' | 'left'}} + */ +export function CardDecorator({ className = '', position = 'top', ...props }) { + const getCardStyle = (pos) => { + if (pos == 'top') { + return 'card-decorator-top'; + } else if (pos == 'left') { + return 'card-decorator-left'; + } else { + console.error('Invalid card position.') + } + } + return ( +
+ ); +} diff --git a/src/components/groups/assignments/create-challenge.jsx b/src/components/groups/assignments/create-challenge.jsx index fafe46db..00fd5d6a 100644 --- a/src/components/groups/assignments/create-challenge.jsx +++ b/src/components/groups/assignments/create-challenge.jsx @@ -7,8 +7,8 @@ import { MarkdownViewer } from '@/components/MarkdownViewer'; import { ToastContainer, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import fileApi from '@/utils/file-api'; -import { getAuth } from 'firebase/auth'; -import request from '@/utils/request'; +import request, { getCookie } from '@/utils/request'; +import { jwtDecode } from 'jwt-decode'; const styles = { h1: { fontSize: '2.4rem' }, @@ -19,8 +19,6 @@ const styles = { h6: { fontSize: '1.2rem' }, }; -const auth = getAuth(); - export default function Createchall(props) { const pages = [ @@ -87,7 +85,10 @@ export default function Createchall(props) { await uploadChallenge(''); return; } else { - const token = await auth.currentUser.accessToken; + const cookie = getCookie('idToken'); + const data = jwtDecode(cookie); + + const token = data.id; const fileId = await fileApi(token, selectedFile); if(fileId !== null) { await uploadChallenge(fileId); diff --git a/src/components/groups/assignments/updateChallengeInfo.jsx b/src/components/groups/assignments/updateChallengeInfo.jsx index 5b4e96d5..b7f383b5 100644 --- a/src/components/groups/assignments/updateChallengeInfo.jsx +++ b/src/components/groups/assignments/updateChallengeInfo.jsx @@ -5,14 +5,12 @@ import { StandardNav } from '@/components/StandardNav'; import ClassroomNav from '@/components/groups/classroomNav'; import CreateAssignment from '@/components/groups/assignments/createAssignment'; import { useState, useEffect } from 'react'; -import request from '@/utils/request'; import { ToastContainer, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; - import fileApi, { deleteFiles, getFileName, getFile } from '@/utils/file-api'; -import { getAuth } from 'firebase/auth'; -const auth = getAuth(); +import request, { getCookie } from '@/utils/request'; +import { jwtDecode } from 'jwt-decode'; const Editor = (props) => { const [contentPreview, setContentPreview] = useState(''); @@ -55,8 +53,9 @@ const Editor = (props) => { if(!validateNewChallege()) { return; } - - const token = await auth.currentUser.accessToken; + const cookie = getCookie('idToken'); + const data = jwtDecode(cookie); + const token = data.id; setIsCreating(true); let fileIds = []; let idsToDelete = []; diff --git a/src/components/home/CountUpOnScroll.jsx b/src/components/home/CountUpOnScroll.jsx deleted file mode 100644 index fb739e76..00000000 --- a/src/components/home/CountUpOnScroll.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import { useRef, useEffect, useState } from 'react'; -import CountUp from 'react-countup'; - -export function CountUpScroll({ item }) { - const [isInView, setIsInView] = useState(false); - const countUpRef = useRef(null); - - useEffect(() => { - const options = { - root: null, - rootMargin: '0px', - threshold: 0.5, - }; - - const observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - setIsInView(true); - observer.unobserve(entry.target); - } - }); - }, options); - - if (countUpRef.current) { - observer.observe(countUpRef.current); - } - - return () => { - if (countUpRef.current) { - observer.unobserve(countUpRef.current); - } - }; - }, [countUpRef]); - - return ( -
- {isInView ? ( - - ) : ( - '0' - )} - + {item.sttype} -
- ); -} diff --git a/src/components/home/FeaturePanel.jsx b/src/components/home/FeaturePanel.jsx index 85fc43f5..1fa9e8bd 100644 --- a/src/components/home/FeaturePanel.jsx +++ b/src/components/home/FeaturePanel.jsx @@ -38,7 +38,7 @@ export function FeaturesPanel() { className="overflow-hidden py-24 sm:py-32" style={{ backgroundColor: '#212121' }} > -
+

diff --git a/src/components/home/GP.jsx b/src/components/home/GP.jsx new file mode 100644 index 00000000..35d169ca --- /dev/null +++ b/src/components/home/GP.jsx @@ -0,0 +1,109 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faBookOpen, faBolt, faComments, faTrophy, faPenFancy, faLightbulb } from '@fortawesome/free-solid-svg-icons'; + +const features = [ + { + name: 'Challenge Writeups', + description: 'Detailed solutions and explanations for various challenges.', + icon: faBookOpen, + }, + { + name: 'Dynamic Labs', + description: 'Engage with interactive and dynamic lab environments instead of static CTFs.', + icon: faBolt, + }, + { + name: 'Comments', + description: 'Share your thoughts and collaborate through comments on each challenge.', + icon: faComments, + }, + { + name: 'Leaderboard', + description: 'See where you stand among peers with our real-time leaderboard.', + icon: faTrophy, + }, + { + name: 'Share your own write ups', + description: 'Contribute your own solutions and help the community grow.', + icon: faPenFancy, + }, + { + name: 'Hints', + description: 'Stuck on a problem? Access hints to help guide your solving process.', + icon: faLightbulb, + }, +] + +export default function GP() { + const [isVisible, setIsVisible] = useState(false); + const imgRef = useRef(null); + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + setIsVisible(true); + } + }, + { threshold: 0.5 } // Trigger when 50% of the element is in view + ); + + if (imgRef.current) { + observer.observe(imgRef.current); + } + + return () => { + if (imgRef.current) { + observer.unobserve(imgRef.current); + } + }; + }, [imgRef]); + + return ( +

+
+
+

CTFGuide Practice Range

+

Never run out of practice material.

+

+ Access hundreds of challenges, writeups, and dynamic labs to help you improve your skills and prepare for competitions. +

+
+
+
+
+ {isVisible && ( + App screenshot + )} + + +
+
+
+ {features.map((feature) => ( +
+
+
{' '} +
{feature.description}
+
+ ))} +
+
+
+ ) +} + diff --git a/src/components/home/Hero.jsx b/src/components/home/Hero.jsx index c5e59cbd..66a3bf6e 100644 --- a/src/components/home/Hero.jsx +++ b/src/components/home/Hero.jsx @@ -8,9 +8,7 @@ import Banner from '@/components/home/Banner'; const navigation = [ - { name: 'Careers', href: '../careers' }, - { name: 'Open Source', href: 'https://github.com/ctfguide-tech' }, - { name: 'Practice', href: '../practice' }, + ] @@ -47,8 +45,13 @@ export function Hero() { ))}
-
- + @@ -127,7 +130,7 @@ export function Hero() {
Get started diff --git a/src/components/home/SecondaryFeatures.jsx b/src/components/home/SecondaryFeatures.jsx index 1719937d..420dc5f1 100644 --- a/src/components/home/SecondaryFeatures.jsx +++ b/src/components/home/SecondaryFeatures.jsx @@ -245,7 +245,7 @@ function FeaturesDesktop() { /> ))} - +
{features.map((feature, featureIndex) => (
+

CTFGuide Education

+

We're the best platform for teaching cybersecurity.

diff --git a/src/components/home/Stats.jsx b/src/components/home/Stats.jsx deleted file mode 100644 index 6f3cc017..00000000 --- a/src/components/home/Stats.jsx +++ /dev/null @@ -1,111 +0,0 @@ -import { - HomeModernIcon, - CommandLineIcon, - SparklesIcon, -} from '@heroicons/react/20/solid'; -import CountUp from 'react-countup'; -import VisibilitySensor from 'react-visibility-sensor'; - -export function Stats() { - const stats = [ - { - name: 'Schools Reached', - stat: 50, - sttype: 'Schools', - icon: HomeModernIcon, - }, - { - name: 'Challenge Attempts', - stat: 10200, - sttype: 'Attempts', - icon: CommandLineIcon, - }, - { - name: 'Total Challenges Solved', - stat: 1346, - sttype: 'Solved', - icon: SparklesIcon, - }, - ]; - - return ( -
-
-

- Perfect for Beginners and Pros Alike -

-

- At CTFGuide, our community provides a supportive environment for - skills development, collaboration, and knowledge sharing.

-

The diversity of perspectives enriches the learning - experience and contributes to CTFGuide's growth and success. -

{' '} -
- {stats.map((item) => ( -
-
-
- {item.name} -
- -
-
- - {({ countUpRef, start }) => ( - - - - )} - - + {item.sttype} -
-
- ))} -
-
- - -

- Trusted by the people from these organizations -

-
- Transistor - Reform - Tuple - -
-
- - ); -} diff --git a/src/components/learn/CountUp.jsx b/src/components/learn/CountUp.jsx deleted file mode 100644 index a39107a5..00000000 --- a/src/components/learn/CountUp.jsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useEffect, useState } from 'react'; -import CountUp from 'react-countup'; - -const CountUpNumber = ({ className, end, duration }) => { - const [start, setStart] = useState(0); - - useEffect(() => { - let timer; - if (start < end) { - const difference = end - start; - const increment = Math.ceil(difference / (duration * 60)); - timer = setInterval(() => { - if (start + increment >= end) { - setStart(end); - clearInterval(timer); - } else { - setStart(start + increment); - } - }, 1000 / 60); - } else { - setStart(end); - } - return () => clearInterval(timer); - }, [start, end, duration]); - - return ( - - - - ); -}; - -export default CountUpNumber; diff --git a/src/components/learn/LearnNav.jsx b/src/components/learn/LearnNav.jsx index 66369332..d1b1a5ff 100644 --- a/src/components/learn/LearnNav.jsx +++ b/src/components/learn/LearnNav.jsx @@ -1,7 +1,7 @@ import { DonutChart } from '@tremor/react'; import Link from 'next/link'; import { useState, useEffect } from 'react'; -import CountUp from 'react-countup'; + import request from '@/utils/request'; export function LearnNav({ navElements, lessonNum }) { @@ -65,10 +65,7 @@ export function LearnNav({ navElements, lessonNum }) { showAnimation={true} />

- + %

Lesson Progress

diff --git a/src/components/moderation/ViewChallenge.jsx b/src/components/moderation/ViewChallenge.jsx new file mode 100644 index 00000000..79041cca --- /dev/null +++ b/src/components/moderation/ViewChallenge.jsx @@ -0,0 +1,157 @@ +import React, { Fragment } from 'react'; +import { Dialog, Transition } from '@headlessui/react'; +import { XMarkIcon } from '@heroicons/react/24/outline'; +import { useEffect, useState } from 'react'; +import request from '@/utils/request'; + +const ViewChallenge = ({ open, setOpen, selected }) => { + + const [challenge, setChallenge] = useState({ + "title": "Loading...", + "content": "Loading...", + "solution": "Loading...", + "difficulty": "Loading..." + }); + + useEffect(() => { + const fetchChallengeData = async () => { + try { + const response = await request(`${process.env.NEXT_PUBLIC_API_URL}/challenges/${selected}`, "GET"); + console.log("run"); + console.log(response); + setChallenge(response.body); + } catch (error) { + console.error(error); + } + }; + + fetchChallengeData(); + }, [selected]); + + return ( + + + +
+ +
+
+
+ + + +
+
+
+
+ { challenge && + { challenge.title || "Loading..." } + } +
+ +
+
+
+
+ + +
+
+

Description

+

{challenge && challenge.content}

+
+
+

Flag

+

{challenge && challenge.solution || "N/A"}

+
+
+

Difficulty

+

{challenge && challenge.difficulty}

+
+ + + + + +
+

Category

+

{challenge && challenge.category}

+ +
+ +
+

Date of Creation

+

{challenge && new Date(challenge.createdAt).toLocaleString([], { hour: 'numeric', minute: 'numeric', hour12: true, month: 'short', day: 'numeric' })}

+ +
+ +
+

Last Updated

+

{challenge && new Date(challenge.updatedAt).toLocaleString([], { hour: 'numeric', minute: 'numeric', hour12: true, month: 'short', day: 'numeric' })}

+ +
+
+ +

Moderator Notes

+ +
+
+ + + +
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+ ); +}; + +export default ViewChallenge; diff --git a/src/components/nav/SearchModal.jsx b/src/components/nav/SearchModal.jsx new file mode 100644 index 00000000..50bb5649 --- /dev/null +++ b/src/components/nav/SearchModal.jsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSearch, faBug, faLock, faUserSecret, faNetworkWired, faBrain, faTerminal } from '@fortawesome/free-solid-svg-icons'; +import { useState, useEffect } from 'react'; +import request from '@/utils/request'; +import router from 'next/router'; + +const SearchModal = ({ showSearchModal, setShowSearchModal }) => { + const [search, setSearch] = useState(''); + const [results, setResults] = useState(null); + const debouncedSearchTerm = useDebounce(search, 100); // We may want to slow this down in prod + + useEffect(() => { + if (debouncedSearchTerm) { + const endpoint = process.env.NEXT_PUBLIC_API_URL; + const url = `${endpoint}/search/${debouncedSearchTerm}`; + console.log(url); + request(url, "GET", null).then((res) => { + setResults(res.results); + console.log(res); + }).catch((err) => { console.log(err); }); + } + }, [debouncedSearchTerm]); + + const routeToChallenge = (id) => router.push("/challenges/"+id); + const routeToUser = (username) => router.push("/users/"+username); + + return ( + <> + {showSearchModal && ( +
setShowSearchModal(true)}> +
setShowSearchModal(false)} + > +
e.stopPropagation()} + > +
+
+ + { + setSearch(e.target.value) + if(e.target.value === '') setResults(null); + }} placeholder="Search for challenges, users, or competitions" className='w-full border-0 text-xl focus:ring-0 bg-transparent text-white' autoFocus /> +
+ + { + results && search && ( +
+
+ {results.challenges.map((result, index) => ( +
routeToChallenge(result.id)}> +

{result.title}

+
+ ))} + {results.users.map((result, index) => ( +
routeToUser(result.username)}> +

{result.username}

+
+ ))} +
+
+ ) + } + +

Search by Category

+
+ + + + + + +
+
+
{/* Content goes here */}
+
+
+
+ )} + + ); +}; + +function useDebounce(value, delay) { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} + +export default SearchModal; + diff --git a/src/components/nav/SpawnTerminal.jsx b/src/components/nav/SpawnTerminal.jsx new file mode 100644 index 00000000..465f52cb --- /dev/null +++ b/src/components/nav/SpawnTerminal.jsx @@ -0,0 +1,154 @@ +import React, { Fragment } from 'react'; +import { Dialog, Transition } from '@headlessui/react'; +import { XMarkIcon } from '@heroicons/react/24/outline'; +import { useEffect, useState } from 'react'; + +const SpawnTerminal = ({ open, setOpen }) => { + + let proMachines = ["Kali Linux (CTFGuide Pro)"]; + + + + const [requiresUpgrade, setRequiresUpgrade] = useState(false); + const [activeSub, setActiveSub] = useState("Ubuntu 22.10 LTS"); + + function hasPermission(e) { + + // Also handle input change + + setActiveSub(e); + + if (proMachines.includes(e)) { + setRequiresUpgrade(true) + } else { + setRequiresUpgrade(false); + } + + } + + + + return ( + + + +
+ +
+
+
+ + +
+
+
+
+ + Launch a machine + +
+ +
+
+
+
+

Choose an operating system

+
+ +
+ + + { + + requiresUpgrade && ( +
+

You need CTFGuide Pro to use this operating system.

+ +
+ ) + } + +

Container Interaction

+
+
+

WebVNC

+
+
+

ShellInABox

+
+
+ +

Import Files

+
+
+ +
+ +

or drag and drop

+
+

.zip

+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ ); +}; + +export default SpawnTerminal; diff --git a/src/components/nav/Upgrade.jsx b/src/components/nav/Upgrade.jsx new file mode 100644 index 00000000..fdac668f --- /dev/null +++ b/src/components/nav/Upgrade.jsx @@ -0,0 +1,85 @@ +import { CheckIcon } from '@heroicons/react/20/solid' + +const includedFeatures = [ + 'Priority machine access', + 'Machines with GUI', + 'Access to more operating systems', + 'Longer machine times', + 'CTFGuide Pro flair on your profile, comments, and created content' + +] + +export default function Upgrade({ open, setOpen }) { + const hideModal = () => setOpen(false); + + return ( +
+
+
+
+
+

Upgrade to CTFGuide Pro

+ +
+
+
+

Monthly Subscription

+

+ Enjoy our core features for free and upgrade to get perks like priority access to terminals, custom container images, customization perks, and more! + +

+
+

What's included

+
+
+
    + {includedFeatures.map((feature) => ( +
  • +
  • + ))} +
+ + +
+ +
+
+
+

Billed monthly

+

+ $5 + USD +

+ + Subscribe + +

+ Invoices and receipts available for easy company reimbursement +

+ + + +
+ + +
+
+
+ +
+
+
+
+ ) +} + diff --git a/src/components/onboarding/DataAsk.jsx b/src/components/onboarding/DataAsk.jsx index 15e53412..3c8f1f49 100644 --- a/src/components/onboarding/DataAsk.jsx +++ b/src/components/onboarding/DataAsk.jsx @@ -1,33 +1,20 @@ -import { Container } from '@/components/Container'; + import { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; -import { getCookie } from '@/utils/request'; import AuthFooter from '@/components/auth/AuthFooter'; -import Link from 'next/link'; - import { ToastContainer, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; -import { getAuth, onAuthStateChanged, signOut } from 'firebase/auth'; - - -export function DataAsk({ props }) { +export function DataAsk(props) { const router = useRouter(); const [username, setUsername] = useState(''); const [validationMessage, setValidationMessage] = useState(''); const [isLoading, setIsLoading] = useState(false); const [userHasEdited, setUserHasEdited] = useState(false); // New state to track if the user has edited the input + function logout() { - signOut(auth) - .then(() => { - window.location.replace('/login'); - }) - .catch((error) => { - console.log(error); - }); + router.push('/login'); } - const auth = getAuth(); - useEffect(() => { if (router.query.part == '1') { @@ -37,6 +24,7 @@ export function DataAsk({ props }) { } } }); + useEffect(() => { if (!userHasEdited) return; // Don't validate until the user edits the input @@ -56,7 +44,7 @@ export function DataAsk({ props }) { } }, [username, userHasEdited]); - function submitData() { + async function submitData() { setIsLoading(true); // Generate JSON to send var username = document.getElementById('username').value; @@ -122,55 +110,43 @@ export function DataAsk({ props }) { return; } - // send http request - var xhr = new XMLHttpRequest(); - xhr.open('POST', `${process.env.NEXT_PUBLIC_API_URL}/users`); - xhr.setRequestHeader('Content-Type', 'application/json'); - - let token = getCookie(); - xhr.setRequestHeader('Authorization', 'Bearer ' + token); - - xhr.withCredentials = true; - - xhr.addEventListener('readystatechange', function () { - if (this.readyState === 4 && this.readyState === 201) { - var parsed = JSON.parse(this.responseText); - if (parsed.username) { - // Sign out - // Redirect to login - window.location.href = '/dashboard'; - } - } - - if (this.readyState === 4 && this.readyState != 201) { - var parsed = JSON.parse(this.responseText); - - if (parsed.error === 'undefined' || parsed.error) { - - - setIsLoading(false); - toast.error(parsed.error); + const body = { + email: props.email, + password: props.password, + username, + birthday, + firstName: firstname, + lastName: lastname, + location: "???", + accountType: props.accountType + } - } else { - window.location.href = "./dashboard"; - // window.location.replace('./onboarding?part=1&error=' + parsed.error); - // document.getElementById('error').classList.remove('hidden'); - // document.getElementById('error').innerHTML = parsed.error; - } + try { + const url = `${process.env.NEXT_PUBLIC_API_URL}/account/register`; + const requestOptions = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }; + + const response = await fetch(url, requestOptions); + const data = await response.json(); + + if(data.success) { + const { token } = data; + setIsLoading(false); + document.cookie = `idToken=${token}; SameSite=None; Secure; Path=/`; + router.push('/dashboard'); + } else { + setIsLoading(false); + toast.error(data.error); } - }); - xhr.send( - JSON.stringify({ - username: localStorage.getItem('username'), - birthday: localStorage.getItem('birthday'), - firstName: localStorage.getItem('firstname'), - lastName: localStorage.getItem('lastname'), - location: "????", - }) - ); + } catch (error) { + console.log(error); + toast.error("An error occurred. Please try again later."); } - + } } @@ -180,40 +156,26 @@ export function DataAsk({ props }) { backgroundRepeat: 'repeat', width: '100%', height: '100%', - }}> + }}> +
+
-
- - - - -
- - - -
+ className=" pb-10 pt-4 px-4 shadow sm:px-10 border-t-4 border-blue-600 bg-neutral-800" + > +
- +
+

+ {' '} + Finish creating your account +

-
-

- {' '} - Finish creating your account -

-
- -
+ adow-sm">
{ - setUsername(e.target.value); - if (!userHasEdited) setUserHasEdited(true); // Set to true on first edit - }} - className="bg-neutral-900 mt-2 block w-full rounded border-0 p-0 py-1 px-4 text-white placeholder-gray-500 focus:ring-0 sm:text-sm" - placeholder="This is what people on CTFGuide will know you as." - /> - {userHasEdited && validationMessage && ( -
- {validationMessage} -
- )} + type="text" + name="name" + id="username" + value={username} + onChange={(e) => { + setUsername(e.target.value); + if (!userHasEdited) setUserHasEdited(true); // Set to true on first edit + }} + className="bg-neutral-900 mt-2 block w-full rounded border-0 p-0 py-1 px-4 text-white placeholder-gray-500 focus:ring-0 sm:text-sm" + placeholder="This is what people on CTFGuide will know you as." + /> + {userHasEdited && validationMessage && ( +
+ {validationMessage} +
+ )}
@@ -319,13 +281,13 @@ export function DataAsk({ props }) { submitData(); }} className="flex w-full justify-center rounded-sm border border-transparent bg-blue-700 hover:bg-blue-700/90 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" - > + > { isLoading ? ( ) : ( - Start Hacking - ) + Start Hacking + ) } @@ -336,8 +298,8 @@ export function DataAsk({ props }) {
- -
+ +
diff --git a/src/components/onboarding/OnboardingFlow.jsx b/src/components/onboarding/OnboardingFlow.jsx index 087e4501..c5ae51b2 100644 --- a/src/components/onboarding/OnboardingFlow.jsx +++ b/src/components/onboarding/OnboardingFlow.jsx @@ -4,7 +4,7 @@ import { DataAskPart2 } from '@/components/onboarding/DataAskPart2'; import { Demo } from '@/components/onboarding/Demo'; import { useRouter } from 'next/router'; -export function OnboardingFlow() { +export function OnboardingFlow(props) { const router = useRouter(); const [flowState, setFlowState] = useState(router.query.part || '1'); @@ -19,15 +19,13 @@ export function OnboardingFlow() { setFlowState(router.query.part); }, [router.query.part]); - // Read part from URL query parameter - if (flowState === '1') { - return ; + return ; } else if (flowState === '2') { return ; } else if (flowState === '3') { return ; } else { - return ; + return ; } } diff --git a/src/components/practice/PracticeNav.jsx b/src/components/practice/PracticeNav.jsx index ddf9d4b0..ba1df44f 100644 --- a/src/components/practice/PracticeNav.jsx +++ b/src/components/practice/PracticeNav.jsx @@ -5,11 +5,11 @@ export function PracticeNav() { return ( <>

- +
  • - Join our community! - + Join our community! +
  • diff --git a/src/components/practice/community.jsx b/src/components/practice/community.jsx index 50ab5dae..69ad5e6c 100644 --- a/src/components/practice/community.jsx +++ b/src/components/practice/community.jsx @@ -1,5 +1,113 @@ import { useState, useEffect } from 'react'; -import Challenge from '../challenge/ChallengeComponent'; +import ChallengeCard from '../profile/ChallengeCard'; +import { Fragment } from 'react'; +import { Listbox, Transition } from '@headlessui/react'; +import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/24/solid'; + +function getCategoryIcon(category) { + switch (category.toLowerCase()) { + case 'forensics': + return 'fas fa-binoculars'; + case 'cryptography': + return 'fas fa-lock'; + case 'web': + return 'fas fa-globe'; + case 'reverse engineering': + return 'fas fa-tools'; + case 'programming': + return 'fas fa-code'; + case 'pwn': + return 'fas fa-skull-crossbones'; + case 'steganography': + return 'fas fa-image'; + case 'basic': + return 'fas fa-graduation-cap'; + default: + return 'fas fa-question'; + } +} + +function CategorySelect({ category, setCategory }) { + const categories = [ + { name: 'All', value: 'all' }, + { name: 'Forensics', value: 'forensics' }, + { name: 'Cryptography', value: 'cryptography' }, + { name: 'Web', value: 'web' }, + { name: 'Reverse Engineering', value: 'reverse engineering' }, + { name: 'Programming', value: 'programming' }, + { name: 'Pwn', value: 'pwn' }, + { name: 'Steganography', value: 'steganography' }, + { name: 'Basic', value: 'basic' }, + ]; + + return ( + + {({ open }) => ( + <> +
    + + + + {categories.find(c => c.value === category).name} + + + + + + + + {categories.map((category) => ( + + `relative cursor-default select-none py-2 pl-3 pr-9 ${active ? 'bg-blue-600 text-white' : 'text-white' + }` + } + value={category.value} + > + {({ selected, active }) => ( + <> +
    + + + {category.name} + +
    + + {selected ? ( + + + ) : null} + + )} +
    + ))} +
    +
    +
    + + )} +
    + ); +} export function Community({ challenges }) { const [difficulty, setDifficulty] = useState('all'); @@ -38,162 +146,138 @@ export function Community({ challenges }) { setFilter(event.target.value); }; - - return ( <> -
    -
    - - -
    -
    - - -
    -
    - - +
    +

    Community Challenges

    +
    + +
    + +
    +
    + + +
    + +
    + + +
    + + +
    + +
    +
    + + +
    -
    -
    -

    Community Challenges

    -
    - {results.length > 0 - ? results - .filter((challenge) => { - if ( - difficulty.toLowerCase() !== 'all' && - challenge.difficulty.toLowerCase() !== difficulty.toLowerCase() - ) { - return false; - } - if ( - filter !== '' && - challenge.category.includes(filter.toLowerCase()) - ) { - return true; - } - if ( - filter !== '' && - !( - challenge.title - .toLowerCase() - .includes(filter.toLowerCase()) || - challenge.content - .toLowerCase() - .includes(filter.toLowerCase()) - ) - ) { - return false; - } - return true; - }) - .map((challenge, index) => ( - - )) - : challenges - .filter((challenge) => { - if ( - difficulty.toLowerCase() !== 'all' && - challenge.difficulty.toLowerCase() !== difficulty.toLowerCase() - ) { - return false; - } - if ( - filter !== '' && - challenge.category.includes(filter.toLowerCase()) - ) { - return true; - } - if ( - filter !== '' && - !( - challenge.title - .toLowerCase() - .includes(filter.toLowerCase()) || - challenge.content - .toLowerCase() - .includes(filter.toLowerCase()) - ) - ) { - return false; - } +
    + {challenges && (results.length > 0 ? results : challenges) + .filter((challenge) => { + if ( + difficulty.toLowerCase() !== 'all' && + challenge.difficulty.toLowerCase() !== difficulty.toLowerCase() + ) { + return false; + } + if ( + filter !== '' && + challenge.category.includes(filter.toLowerCase()) + ) { return true; - }) - .map((challenge, index) => ( - - ))} + } + if ( + filter !== '' && + !( + challenge.title + .toLowerCase() + .includes(filter.toLowerCase()) || + challenge.content + .toLowerCase() + .includes(filter.toLowerCase()) + ) + ) { + return false; + } + return true; + }) + .map((challenge) => ( + + )) + }
    diff --git a/src/components/profile/ChallengeCard.jsx b/src/components/profile/ChallengeCard.jsx index fd854a28..13d73db2 100644 --- a/src/components/profile/ChallengeCard.jsx +++ b/src/components/profile/ChallengeCard.jsx @@ -1,76 +1,67 @@ import React from 'react'; -import { Tooltip } from 'react-tooltip'; +import { CardDecorator } from '../design/CardDecorator'; +import Link from 'next/link'; +import Skeleton from 'react-loading-skeleton'; -const ChallengeCard = ({ id, title, category, difficulty, createdAt, creator, views, likes }) => { +/** + * @param {import('react').HTMLAttributes & { challenge: {id: string, title: string, category: string, difficulty: string, createdAt: string, creator: string, views: number, likes: number} }} props + * */ +const ChallengeCard = (_props) => { + const { challenge, ...props } = _props; + const baseUrl = process.env.NEXT_PUBLIC_FRONTEND_URL; + const dateFormatted = (date) => + new Date(date) + .toLocaleDateString('en-US', { + month: '2-digit', + day: '2-digit', + year: 'numeric', + }) - const baseUrl = process.env.NEXT_PUBLIC_FRONTEND_URL; + const colorBG = { + 'BEGINNER': 'group-hover:bg-blue-500', + 'EASY': 'group-hover:bg-green-500', + 'MEDIUM': 'group-hover:bg-orange-500', + 'HARD': 'group-hover:bg-red-500', + 'INSANE': 'group-hover:bg-purple-500', + }; + const colorText = { + 'BEGINNER': 'bg-blue-500 text-blue-50', + 'EASY': 'bg-green-500 text-green-50', + 'MEDIUM': 'bg-orange-500 text-orange-50', + 'HARD': 'bg-red-500 text-red-50', + 'INSANE': 'bg-purple-500 text-purple-50', + }; - let color; - const colors = ['blue-600', 'green-600', 'orange-600', 'red-600', 'purple-400']; - - if (!difficulty) difficulty = 'BEGINNER'; - - if (difficulty === 'BEGINNER') { - color = colors[0]; - } else if (difficulty === 'EASY') { - color = colors[1]; - } else if (difficulty === 'MEDIUM') { - color = colors[2]; - } else if (difficulty === 'HARD') { - color = colors[3]; - } else if (difficulty === 'INSANE') { - color = colors[4]; - } else { - color = colors[0]; - } - - - return ( -
    - -
    -
    - -
    -
    + return ( + challenge && ( + + +
    +

    {challenge.title}

    +

    {challenge.creator}

    +

    {challenge.difficulty?.toLowerCase() || }

    +
    +

    + + {dateFormatted(challenge.createdAt)} +

    +

    + + {challenge.views} + + {challenge.upvotes} +

    +
    - ) + ) + || ( +
    + + + + +
    ) + ) }; -export default ChallengeCard; \ No newline at end of file +export default ChallengeCard; diff --git a/src/components/settingComponents/generalPage.jsx b/src/components/settingComponents/generalPage.jsx new file mode 100644 index 00000000..bc38069d --- /dev/null +++ b/src/components/settingComponents/generalPage.jsx @@ -0,0 +1,496 @@ + +import React from 'react' +import { useState } from 'react'; +import { Transition, Dialog } from '@headlessui/react'; +import { Fragment } from 'react'; +import { Locations } from '@/components/settingComponents/locations'; +import { useRouter } from 'next/router'; +import { getCookie } from '@/utils/request'; +import { useEffect } from 'react'; + +export default function General() { + const router = useRouter(); + + const [isPopupOpen, setIsPopupOpen] = useState(false); + const [selectedImage, setSelectedImage] = useState(null); + + const [inputText, setInputText] = useState(''); + + const handleInputChange = (event) => { + setInputText(event.target.value); + }; + const [pfp, setPfp] = useState(`https://robohash.org/KshitijIsCool.png?set=set1&size=150x150`); + const [open, setOpen] = useState(true); + + + function pfpChange() { + pfpChanged = true; + } + + const handlePopupOpen = () => { + setIsPopupOpen(true); + } + + useEffect(() => { + const fileInput = document.getElementById('fileInput'); + + // set username + var xhr = new XMLHttpRequest(); + + xhr.addEventListener('readystatechange', function () { + if (this.readyState === 4) { + console.log(this.responseText); + try { + if (document.getElementById('first-name')) { + + setUsername(JSON.parse(this.responseText).username); + } + + } catch (e) { + console.log(e); + } + } + }); + + xhr.open('GET', `${process.env.NEXT_PUBLIC_API_URL}/account`); + let token = getCookie(); + xhr.setRequestHeader('Authorization', 'Bearer ' + token); + xhr.withCredentials = true; + xhr.send(); + }, []); + + const handlePopupClose = () => { + setIsPopupOpen(false); + } + + const handleImageChange = (event) => { + const file = event.target.files[0]; + setSelectedImage(file); + } + + const handleSaveChanges = async () => { + if (!selectedImage) { + console.log("No image selected"); + setIsPopupOpen(false) + return; + } + + + // upload to firebase storage + try { + const storage = getStorage(); + const metadata = { + contentType: 'image/jpeg', + }; + + const storageRef = ref(storage, `${email}/pictures/pfp`); + const uploadTask = uploadBytesResumable(storageRef, selectedImage, metadata) + + uploadTask.on('state_changed', + (snapshot) => { + // progress function + const progress = Math.round((snapshot.bytesTransferred / snapshot.totalBytes) * 100); + console.log('Upload is ' + progress + '% done'); + switch (snapshot.state) { + case 'paused': + console.log('Upload is paused'); + break; + case 'running': + console.log('Upload is running'); + break; + } + }, + (error) => { + switch (error.code) { + case 'storage/unauthorized': + console.log('User does not have permission to access the object'); + break; + case 'storage/canceled': + console.log('User canceled the upload'); + break; + case 'storage/unknown': + console.log('Unknown error occurred, inspect error.serverResponse'); + break; + } + }, + async () => { + const imageUrl = await getDownloadURL(uploadTask.snapshot.ref) + console.log(imageUrl) + console.log(user); + const endPoint = process.env.NEXT_PUBLIC_API_URL + '/users/' + username + '/updatePfp'; + const body = { imageUrl } + const response = await request(endPoint, "POST", body); + console.log("Here is the result: ", response) + if (response.success) { + console.log("profile picture uploaded successfully"); + } else { + console.log("Failed to upload profile picture"); + } + window.location.reload(); + + } + ); + setIsPopupOpen(false); + + + } catch (err) { + console.log(err); + console.log("An error occured while uploading profile picture"); + } + } + + const handleClick = () => { } + + function saveGeneral() { + document.getElementById('save').innerHTML = 'Saving...'; + + var firstName = document.getElementById('first-name').value; + var lastName = document.getElementById('last-name').value; + var bio = document.getElementById('bio').value; + var github = document.getElementById('url').value; + var location = document.getElementById('location').value; + + var data = JSON.stringify({ + bio: bio, + githubUrl: github, + firstName: firstName, + lastName: lastName, + location: location, + }); + + var xhr = new XMLHttpRequest(); + + xhr.addEventListener('readystatechange', function () { + if (this.readyState === 4) { + console.log(this.responseText); + document.getElementById('save').innerHTML = 'Save'; + } + }); + + xhr.open('PUT', `${process.env.NEXT_PUBLIC_API_URL}/account`); + xhr.setRequestHeader('Content-Type', 'application/json'); + let token = getCookie(); + xhr.setRequestHeader('Authorization', 'Bearer ' + token); + xhr.withCredentials = true; + xhr.send(data); + + } + + return ( +
    +
    +

    + General +

    + +
    +
    +
    +

    + Profile +

    +

    + This information will be displayed publicly so be + careful what you share. +

    +
    + +
    + + +
    + +
    + + +
    + +
    + +
    + +
    +
    + +
    + +
    + photo + + + + +
    +
    + +
    + +
    + - - -

    - Error posting comment! This could be because it was less than 5 - characters or greater than 250 characters.{' '} -

    - {comments.map((message, index) => ( -
    -

    - @{message.username} -

    -

    - {message.content} -

    - - Report Comment - -

    -
    - ))} -
    -
    - {/* ***************************************** */} - - - -
    - - - - - {/* This element is to trick the browser into centering the modal contents. */} - - -
    -
    -
    - {submissionMsg == 'success' && ( - - - - )} - - {submissionMsg == 'incorrect' && ( - - - - )} -
    -
    - - {submissionMsg == 'success' - ? "Nice hackin', partner!" - : ''} - {submissionMsg == 'incorrect' - ? 'Submission incorrect!' - : ''} - {submissionMsg == 'error' ? 'Login to submit a flag' : ''} - -
    -

    - {submissionMsg == 'success' - ? `You were awarded ${award} points.` - : ''} - {submissionMsg == 'incorrect' - ? `Incorrect submission, no points awarded.` - : ''} -

    -
    -
    -
    -
    - - -
    -
    -
    -
    -
    -
    - - - -
    - -
    -
    -
    - - -
    -
    -
    - -

    Hints

    -
    -
    -
    - -
    -
    -
    -

    - {' '} - On our main platform, each hint incurs a 10% penalty - and each submission a 3% penalty. -

    -
    -
    -

    - You - must be logged in to see hints! -

    -
    -
    -
    - {hintMessages ? ( -
    - {hintMessages.map((hint, index) => ( -
    -

    - - Hint #{index + 1} - - {hint &&

    {hint}

    } -

    -
    - ))} -
    - -
    -
    - ) : ( -

    - Oops, no hints for this challenge. -

    - )} -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    - ℹ We provide accessible environments for everyone to run cybersecurity - tools. Abuse and unnecessary computation is prohibited. -

    - -