diff --git a/app/ctf/miniapi/packet1/route.ts b/app/ctf/miniapi/packet1/route.ts
new file mode 100644
index 00000000..72f7821c
--- /dev/null
+++ b/app/ctf/miniapi/packet1/route.ts
@@ -0,0 +1,14 @@
+import { NextResponse } from "next/server";
+
+export async function GET() {
+ return NextResponse.json({
+ fragment: "8720414d7ad2bdb65cd9",
+ param: "signal",
+ meta: {
+ processedAt: 1700000300,
+ serverId: "node-7f3a",
+ region: "us-east-2",
+ latency: 42
+ }
+ });
+}
diff --git a/app/ctf/miniapi/packet2/route.ts b/app/ctf/miniapi/packet2/route.ts
new file mode 100644
index 00000000..76574869
--- /dev/null
+++ b/app/ctf/miniapi/packet2/route.ts
@@ -0,0 +1,14 @@
+import { NextResponse } from "next/server";
+
+export async function GET() {
+ return NextResponse.json({
+ fragment: "5b0125706439da",
+ param: "signal",
+ meta: {
+ processedAt: 1700000100,
+ serverId: "node-2b9c",
+ region: "us-west-1",
+ latency: 38
+ }
+ });
+}
diff --git a/app/ctf/miniapi/packet3/route.ts b/app/ctf/miniapi/packet3/route.ts
new file mode 100644
index 00000000..a5430530
--- /dev/null
+++ b/app/ctf/miniapi/packet3/route.ts
@@ -0,0 +1,14 @@
+import { NextResponse } from "next/server";
+
+export async function GET() {
+ return NextResponse.json({
+ fragment: "ee3111b1dd03a7",
+ param: "signal",
+ meta: {
+ processedAt: 1700000400,
+ serverId: "node-1a4d",
+ region: "eu-central-1",
+ latency: 91
+ }
+ });
+}
diff --git a/app/ctf/miniapi/packet4/route.ts b/app/ctf/miniapi/packet4/route.ts
new file mode 100644
index 00000000..8efde622
--- /dev/null
+++ b/app/ctf/miniapi/packet4/route.ts
@@ -0,0 +1,14 @@
+import { NextResponse } from "next/server";
+
+export async function GET() {
+ return NextResponse.json({
+ fragment: "1420c4108b01de39",
+ param: "signal",
+ meta: {
+ processedAt: 1700000200,
+ serverId: "node-9e1f",
+ region: "ap-southeast-1",
+ latency: 67
+ }
+ });
+}
diff --git a/app/ctf/miniapi/route.ts b/app/ctf/miniapi/route.ts
new file mode 100644
index 00000000..70dd6ae3
--- /dev/null
+++ b/app/ctf/miniapi/route.ts
@@ -0,0 +1,39 @@
+import { NextResponse } from "next/server";
+import { derive } from "../utils";
+
+async function sha256(text: string) {
+ const encoder = new TextEncoder();
+ const data = encoder.encode(text);
+
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
+
+ return Array.from(new Uint8Array(hashBuffer))
+ .map(b => b.toString(16).padStart(2, "0"))
+ .join("");
+}
+
+const SERVER_SECRET = derive(["LWtleQ==", "ZWNyZXQ=", "dG9wLXM="]);
+
+export async function GET() {
+ const flag = derive([
+ "MX0=",
+ "MW4xNHA=",
+ "YWc3LW0=",
+ "dGZ7Zmw=",
+ "aGFja2M="
+ ]);
+ const hiddenFlag = derive([
+ "NzNyfQ==",
+ "cDFtNDU=",
+ "YWc4LTQ=",
+ "dGZ7Zmw=",
+ "aGFja2M="
+ ]);
+
+ const secret = await sha256(hiddenFlag + SERVER_SECRET);
+
+ return NextResponse.json({
+ flag,
+ secret
+ });
+}
diff --git a/app/ctf/miniapi/unlock/route.ts b/app/ctf/miniapi/unlock/route.ts
new file mode 100644
index 00000000..0b1d5d34
--- /dev/null
+++ b/app/ctf/miniapi/unlock/route.ts
@@ -0,0 +1,63 @@
+import { NextResponse } from "next/server";
+import { derive } from "../../utils";
+
+async function sha256(text: string) {
+ const encoder = new TextEncoder();
+ const data = encoder.encode(text);
+
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
+
+ return Array.from(new Uint8Array(hashBuffer))
+ .map(b => b.toString(16).padStart(2, "0"))
+ .join("");
+}
+
+const SERVER_SECRET = derive(["LWtleQ==", "ZWNyZXQ=", "dG9wLXM="]);
+
+export async function GET(req: Request) {
+ const url = new URL(req.url);
+ const secret = url.searchParams.get("secret");
+ const signal = url.searchParams.get("signal");
+
+ if (!secret && !signal) {
+ return NextResponse.json(
+ { error: "Missing query parameter" },
+ { status: 400 }
+ );
+ }
+
+ if (secret && signal) {
+ return NextResponse.json(
+ { error: "Only one query parameter allowed" },
+ { status: 400 }
+ );
+ }
+
+ let param = secret ? secret : signal;
+ let hiddenFlag = secret
+ ? derive(["NzNyfQ==", "cDFtNDU=", "YWc4LTQ=", "dGZ7Zmw=", "aGFja2M="])
+ : derive([
+ "fQ==",
+ "aDNjN2Y=",
+ "YjM0Nzc=",
+ "NzV5MHU=",
+ "MG5ncjQ=",
+ "YWc5LWM=",
+ "dGZ7Zmw=",
+ "aGFja2M="
+ ]);
+
+ const expected = await sha256(hiddenFlag + SERVER_SECRET);
+ console.log(expected);
+
+ if (param !== expected) {
+ return NextResponse.json(
+ { error: "Parameter value is incorrect" },
+ { status: 400 }
+ );
+ }
+
+ return NextResponse.json({
+ decoded: hiddenFlag
+ });
+}
diff --git a/app/ctf/page.module.scss b/app/ctf/page.module.scss
new file mode 100644
index 00000000..7866dc1a
--- /dev/null
+++ b/app/ctf/page.module.scss
@@ -0,0 +1,140 @@
+.ctfSection {
+ position: relative;
+ width: 100%;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background: linear-gradient(to bottom, #020316, #16133e);
+ z-index: 1;
+}
+
+.ctfBackgrounds {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ z-index: 0;
+
+ -webkit-mask-image: linear-gradient(
+ to bottom,
+ rgba(0, 0, 0, 1) 90%,
+ rgba(0, 0, 0, 0) 100%
+ );
+ mask-image: linear-gradient(
+ to bottom,
+ rgba(0, 0, 0, 1) 90%,
+ rgba(0, 0, 0, 0) 100%
+ );
+}
+
+@keyframes tinyStarsGlow {
+ 0% {
+ opacity: 0.2;
+ filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.2));
+ }
+ 50% {
+ opacity: 0.4;
+ filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.8));
+ }
+ 100% {
+ opacity: 0.2;
+ filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.2));
+ }
+}
+
+@media (max-width: 768px) {
+ .ctfSection {
+ min-height: 100vh;
+ }
+}
+
+.flagInput {
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(163, 21, 214, 0.3);
+ }
+
+ &:focus {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 30px rgba(253, 171, 96, 0.4);
+ }
+
+ &.correct {
+ border-color: #4caf50 !important;
+ background: rgba(76, 175, 80, 0.1) !important;
+ }
+
+ &.incorrect {
+ border-color: #f44336 !important;
+ background: rgba(244, 67, 54, 0.1) !important;
+ }
+}
+
+.progressBar {
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 10px;
+ overflow: hidden;
+ height: 12px;
+
+ .progressFill {
+ height: 100%;
+ background: linear-gradient(90deg, #a315d6, #fdab60, #a315d6);
+ background-size: 200% 100%;
+ animation: shimmer 2s linear infinite;
+ transition: width 0.5s ease;
+ }
+}
+
+@keyframes shimmer {
+ 0% {
+ background-position: 200% 0;
+ }
+ 100% {
+ background-position: -200% 0;
+ }
+}
+
+.flagCard {
+ background: rgba(255, 255, 255, 0.08);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-radius: 16px;
+ padding: 20px;
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 12px 35px rgba(163, 21, 214, 0.25);
+ border-color: rgba(163, 21, 214, 0.4);
+ }
+}
+
+.celebration {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ pointer-events: none;
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ & svg {
+ animation: confetti 3s ease-out forwards;
+ }
+}
+
+@keyframes confetti {
+ 0% {
+ transform: scale(0) rotate(0deg);
+ opacity: 1;
+ }
+ 100% {
+ transform: scale(2) rotate(720deg);
+ opacity: 0;
+ }
+}
diff --git a/app/ctf/page.tsx b/app/ctf/page.tsx
new file mode 100644
index 00000000..04db4a8c
--- /dev/null
+++ b/app/ctf/page.tsx
@@ -0,0 +1,1106 @@
+"use client";
+
+import ErrorSnackbar from "@/components/ErrorSnackbar/ErrorSnackbar";
+import Loading from "@/components/Loading/Loading";
+import {
+ Box,
+ Button,
+ Container,
+ Typography,
+ Accordion,
+ AccordionDetails,
+ AccordionSummary
+} from "@mui/material";
+import { ExpandMore } from "@mui/icons-material";
+import { useEffect, useState } from "react";
+import { motion, Variants } from "framer-motion";
+import { GradientButton } from "@/components/GradientButton/GradientButton";
+import { derive } from "./utils";
+
+const TwinklingStar = ({
+ size,
+ top,
+ left,
+ delay,
+ duration
+}: {
+ size: number;
+ top: string;
+ left: string;
+ delay: number;
+ duration: number;
+}) => (
+
+);
+
+const ShootingStar = ({
+ delay,
+ duration
+}: {
+ delay: number;
+ duration: number;
+}) => (
+
+);
+
+const FloatingDebris = ({
+ src,
+ top,
+ left,
+ right,
+ size,
+ delay,
+ duration,
+ rotation
+}: {
+ src: string;
+ top: string;
+ left?: string;
+ right?: string;
+ size: string;
+ delay: number;
+ duration: number;
+ rotation: number;
+}) => (
+
+);
+
+const containerVariants: Variants = {
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: {
+ delayChildren: 0.3,
+ staggerChildren: 0.15
+ }
+ }
+};
+
+const itemVariants: Variants = {
+ hidden: { opacity: 0, y: 30 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.8,
+ ease: [0.25, 0.46, 0.45, 0.94]
+ }
+ }
+};
+
+export default function CTF() {
+ const [loading, setLoading] = useState(true);
+ const [showErrorAlert, setShowErrorAlert] = useState(false);
+ const [errorMessage, setErrorMessage] = useState("");
+ const [stars, setStars] = useState<
+ {
+ id: number;
+ size: number;
+ top: string;
+ left: string;
+ delay: number;
+ duration: number;
+ }[]
+ >([]);
+
+ useEffect(() => {
+ const t = setTimeout(() => setLoading(false), 250);
+
+ const generatedStars = Array.from({ length: 120 }).map((_, i) => ({
+ id: i,
+ size: Math.random() * 4 + 1,
+ top: `${Math.random() * 100}%`,
+ left: `${Math.random() * 100}%`,
+ delay: Math.random() * 8,
+ duration: 2 + Math.random() * 3
+ }));
+ setStars(generatedStars);
+
+ (window as any).$SECRET$ = {
+ flag: (() => {
+ return derive([
+ "Mzd9",
+ "MzUzY3I=",
+ "MG41MDE=",
+ "YWc0LWM=",
+ "dGZ7Zmw=",
+ "aGFja2M="
+ ]);
+ })(),
+ reveal: () => {
+ console.log(
+ derive([
+ "fQ==",
+ "cHIxbjc=",
+ "NDExNzA=",
+ "YWc1LWM=",
+ "dGZ7Zmw=",
+ "aGFja2M="
+ ])
+ );
+ },
+ password: (key: number) => {
+ if (key === (window as any).$MAGIC_NUMBER$.number) {
+ console.log(
+ btoa(
+ derive([
+ "bTN9",
+ "M2MwZDM=",
+ "YWc2LWQ=",
+ "dGZ7Zmw=",
+ "aGFja2M="
+ ])
+ )
+ );
+ } else {
+ console.log(
+ "Authentication failed. What's the magic number?"
+ );
+ }
+ }
+ };
+
+ (window as any).$MAGIC_NUMBER$ = {
+ number: 64
+ };
+
+ (window as any).$HINT_1$ = {
+ hint: "I'm in Elements!"
+ };
+
+ (window as any).$HINT_2$ = {
+ hint: "I might be NOT-DISPLAYED until you check the CSS"
+ };
+
+ (window as any).$HINT_3$ = {
+ hint: "Check your comms, you received a message! I wonder where you can FIND-ME in the DOM"
+ };
+
+ (window as any).$HINT_4$ = {
+ hint: "There might be more than hints stored in the console.. Try entering \'window\'"
+ };
+
+ (window as any).$HINT_5$ = {
+ hint: "There's a lot to this secret. Hint: f is for function"
+ };
+
+ (window as any).$HINT_6$ = {
+ hint: "The password is encoded in magic!"
+ };
+
+ (window as any).$HINT_7$ = {
+ hint: "Psst.. I heard there’s a secret API. Maybe you can ping it from your comms"
+ };
+
+ (window as any).$HINT_8$ = {
+ hint: "A new endpoint! This one requires a secret..."
+ };
+
+ (window as any).$HINT_9$ = {
+ hint: "A signal has been scrambled across multiple transmissions. The truth is in the timing"
+ };
+
+ return () => clearTimeout(t);
+ }, []);
+
+ const pingMiniApi = async () => {
+ try {
+ await fetch("/ctf/miniapi");
+ } catch (e: any) {
+ setErrorMessage(e?.message || "Failed to ping MiniAPI.");
+ setShowErrorAlert(true);
+ }
+ };
+
+ const pingMiniApiAgain = async () => {
+ try {
+ await fetch("/ctf/miniapi/unlock");
+ } catch (e: any) {
+ setErrorMessage(e?.message || "Failed to ping MiniAPI.");
+ setShowErrorAlert(true);
+ }
+ };
+
+ const decryptSignal = async () => {
+ try {
+ const steps = ["packet1", "packet2", "packet3", "packet4"];
+ for (let i = steps.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [steps[i], steps[j]] = [steps[j], steps[i]];
+ }
+ steps.map(step => {
+ fetch(`/ctf/miniapi/${step}`);
+ });
+ } catch (e: any) {
+ setErrorMessage(e?.message || "Failed to decrypt.");
+ setShowErrorAlert(true);
+ }
+ };
+
+ if (loading) return ;
+
+ return (
+
+ setShowErrorAlert(false)}
+ message={errorMessage}
+ anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
+ />
+
+ {stars.map(star => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CAPTURE THE FLAG
+
+
+
+
+
+
+
+
+
+
+
+ Explore, discover, and uncover hidden secrets
+ embedded in this page. Use your browser's
+ developer tools and keen observation skills to find
+ all flags. Every flag you find brings you closer to
+ victory!
+
+
+
+
+
+
+ MISSION OBJECTIVE
+
+
+ Find all 9 hidden flags scattered throughout
+ this page. To get started, right-click anywhere
+ on the page and select Inspect. Keep an
+ eye on the Elements, Console, and{" "}
+ Network tabs.
+
+
+
+ }
+ sx={{
+ fontFamily: "Tsukimi Rounded",
+ fontWeight: 600,
+ color: "#FDAB60"
+ }}
+ >
+ Need hints?
+
+
+
+ Luckily, your mission commander has left you
+ some clues. In your browser's Developer
+ Tools, navigate to the Console tab.
+ Type $HINT_1$ and press Enter to
+ reveal the hint. This works for all 9 hints!
+
+
+
+
+
+
+
+
+ COMMS
+
+
+ YOU HAVE 1 MESSAGE:
+
+
+
+ {derive([
+ "ZzN9",
+ "bTM1NTQ=",
+ "M2NyMzc=",
+ "YWczLTU=",
+ "dGZ7Zmw=",
+ "aGFja2M="
+ ])}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {`YOUR FIRST FLAG HERE: ${derive(["MWdoN30=", "MTQxbjU=", "YWcxLXA=", "dGZ7Zmw=", "aGFja2M="])}`}
+
+
+
+
+ );
+}
diff --git a/app/ctf/submit/page.tsx b/app/ctf/submit/page.tsx
new file mode 100644
index 00000000..7ac23244
--- /dev/null
+++ b/app/ctf/submit/page.tsx
@@ -0,0 +1,1413 @@
+"use client";
+
+import {
+ Box,
+ Container,
+ Typography,
+ TextField,
+ Alert,
+ Button,
+ CircularProgress
+} from "@mui/material";
+import { useState, useEffect, useRef, useCallback } from "react";
+import { motion, Variants } from "framer-motion";
+import Link from "next/link";
+import styles from "./submit.module.scss";
+
+const API_URL = "https://adonix.hackillinois.org";
+
+const FLAG_POINTS = [
+ { range: "1-3", points: 2, color: "#4CAF50" },
+ { range: "4-6", points: 6, color: "#2196F3" },
+ { range: "7-8", points: 10, color: "#FF9800" },
+ { range: "9", points: 16, color: "#E91E63" }
+];
+
+const getFlagPoints = (index: number): number => {
+ if (index < 3) return 2;
+ if (index < 6) return 6;
+ if (index < 8) return 10;
+ return 16;
+};
+
+const getFlagColor = (index: number): string => {
+ if (index < 3) return "#4CAF50";
+ if (index < 6) return "#2196F3";
+ if (index < 8) return "#FF9800";
+ return "#E91E63";
+};
+
+interface FlagStatusType {
+ correct: boolean | null;
+ claimed: boolean;
+ loading: boolean;
+}
+
+const TwinklingStar = ({
+ size,
+ top,
+ left,
+ delay,
+ duration
+}: {
+ size: number;
+ top: string;
+ left: string;
+ delay: number;
+ duration: number;
+}) => (
+
+);
+
+const ShootingStar = ({
+ delay,
+ duration
+}: {
+ delay: number;
+ duration: number;
+}) => (
+
+);
+
+const FloatingPlanet = ({
+ src,
+ top,
+ left,
+ right,
+ size,
+ delay,
+ duration
+}: {
+ src: string;
+ top: string;
+ left?: string;
+ right?: string;
+ size: string | { xs?: string; sm?: string; md?: string; lg?: string };
+ delay: number;
+ duration: number;
+}) => (
+
+);
+
+const containerVariants: Variants = {
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: {
+ delayChildren: 0.3,
+ staggerChildren: 0.1
+ }
+ }
+};
+
+const itemVariants: Variants = {
+ hidden: { opacity: 0, y: 30 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.6,
+ ease: [0.25, 0.46, 0.45, 0.94]
+ }
+ }
+};
+
+export default function CTFSubmit() {
+ const [flagInputs, setFlagInputs] = useState([
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ""
+ ]);
+ const [flagStatus, setFlagStatus] = useState(
+ Array(9)
+ .fill(null)
+ .map(() => ({ correct: null, claimed: false, loading: false }))
+ );
+ const [showSuccess, setShowSuccess] = useState(false);
+ const [isAuthenticated, setIsAuthenticated] = useState(
+ null
+ );
+ const debounceRefs = useRef<(NodeJS.Timeout | null)[]>(Array(9).fill(null));
+ const [stars, setStars] = useState<
+ {
+ id: number;
+ size: number;
+ top: string;
+ left: string;
+ delay: number;
+ duration: number;
+ }[]
+ >([]);
+
+ useEffect(() => {
+ const checkAuth = async () => {
+ try {
+ const response = await fetch(`${API_URL}/auth/token/`, {
+ mode: "cors",
+ credentials: "include"
+ });
+ setIsAuthenticated(response.ok);
+ } catch {
+ setIsAuthenticated(false);
+ }
+ };
+ checkAuth();
+
+ const savedFlags = localStorage.getItem("ctf_flags");
+ if (savedFlags) {
+ const parsedFlags = JSON.parse(savedFlags);
+ const allFlags = Array(9)
+ .fill("")
+ .map((_, i) => parsedFlags[i] || "");
+ setFlagInputs(allFlags);
+ }
+
+ const savedStatus = localStorage.getItem("ctf_status");
+ if (savedStatus) {
+ try {
+ const parsedStatus = JSON.parse(savedStatus);
+ const allStatus = Array(9)
+ .fill(null)
+ .map(
+ (_, i) =>
+ parsedStatus[i] || {
+ correct: null,
+ claimed: false,
+ loading: false
+ }
+ );
+ setFlagStatus(allStatus);
+ } catch {
+ /* ignore */
+ }
+ }
+
+ const generatedStars = Array.from({ length: 100 }).map((_, i) => ({
+ id: i,
+ size: Math.random() * 4 + 1,
+ top: `${Math.random() * 100}%`,
+ left: `${Math.random() * 100}%`,
+ delay: Math.random() * 8,
+ duration: 2 + Math.random() * 3
+ }));
+ setStars(generatedStars);
+ }, []);
+
+ const submitFlag = useCallback(async (index: number, answer: string) => {
+ const flagId = `flag${index + 1}`;
+
+ setFlagStatus(prev => {
+ const newStatus = [...prev];
+ newStatus[index] = { ...newStatus[index], loading: true };
+ return newStatus;
+ });
+
+ try {
+ const response = await fetch(`${API_URL}/ctf/submit/${flagId}/`, {
+ method: "POST",
+ mode: "cors",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ answer: answer.trim() })
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ return { correct: true, claimed: false };
+ }
+ if (data.error === "AlreadyClaimed") {
+ return { correct: true, claimed: true };
+ }
+ return { correct: false, claimed: false };
+ } catch {
+ return { correct: false, claimed: false };
+ }
+ }, []);
+
+ const handleFlagChange = (index: number, value: string) => {
+ const newInputs = [...flagInputs];
+ newInputs[index] = value;
+ setFlagInputs(newInputs);
+ localStorage.setItem("ctf_flags", JSON.stringify(newInputs));
+
+ if (debounceRefs.current[index]) {
+ clearTimeout(debounceRefs.current[index]!);
+ }
+
+ if (!value.trim()) return;
+
+ debounceRefs.current[index] = setTimeout(async () => {
+ const result = await submitFlag(index, value);
+ setFlagStatus(prev => {
+ const newStatus = [...prev];
+ newStatus[index] = {
+ correct: result.correct,
+ claimed: result.claimed,
+ loading: false
+ };
+ localStorage.setItem("ctf_status", JSON.stringify(newStatus));
+ return newStatus;
+ });
+ }, 800);
+ };
+
+ const correctCount = flagStatus.filter(s => s.correct === true).length;
+ const progress = (correctCount / 9) * 100;
+
+ useEffect(() => {
+ setShowSuccess(correctCount === 9);
+ }, [correctCount]);
+
+ if (isAuthenticated === null) {
+ return (
+
+
+
+ );
+ }
+
+ if (!isAuthenticated) {
+ return (
+
+
+ Please log in to submit flags
+
+
+
+ );
+ }
+
+ return (
+
+ {stars.map(star => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SUBMIT YOUR FLAGS
+
+
+
+
+
+ Enter the flags you've discovered. Your
+ progress will be automatically saved. Good luck!
+
+
+
+
+
+
+ Flag Format
+
+
+ hackctf{"{flag#-flagcontent}"}
+
+
+ Submit flags in the exact format. You can submit
+ in any order!
+
+
+
+
+
+
+
+ Point Distribution
+
+
+ {FLAG_POINTS.map((item, index) => (
+
+
+ Flag
+ {item.range.includes("-")
+ ? "s"
+ : ""}{" "}
+ {item.range}
+
+
+ {item.points}pts
+
+
+ ))}
+
+
+
+
+ {showSuccess && (
+
+
+ Congratulations! You've found all the
+ flags!
+
+
+ )}
+
+
+
+
+
+
+ Progress: {correctCount} / {9} flags
+
+
+
+
+
+ Level 1
+
+
+ {flagInputs.slice(0, 3).map((flag, index) => (
+
+
+
+
+ FLAG {index + 1}
+ {flagStatus[index].correct ===
+ true && " (correct)"}
+ {flagStatus[index].correct ===
+ false && " (incorrect)"}
+
+
+ {getFlagPoints(index)}pts
+
+
+
+ handleFlagChange(
+ index,
+ e.target.value
+ )
+ }
+ placeholder="Enter flag..."
+ sx={{
+ fontFamily: "Montserrat",
+ "& .MuiOutlinedInput-root": {
+ fontFamily: "Montserrat",
+ color: "white",
+ backgroundColor:
+ "rgba(0, 0, 0, 0.3)",
+ borderRadius: "8px",
+ "& fieldset": {
+ borderColor:
+ flagStatus[index]
+ .correct ===
+ true
+ ? "#4CAF50"
+ : flagStatus[
+ index
+ ]
+ .correct ===
+ false
+ ? "#F44336"
+ : "rgba(255, 255, 255, 0.3)"
+ },
+ "&:hover fieldset": {
+ borderColor:
+ flagStatus[index]
+ .correct ===
+ true
+ ? "#4CAF50"
+ : flagStatus[
+ index
+ ]
+ .correct ===
+ false
+ ? "#F44336"
+ : "rgba(163, 21, 214, 0.5)"
+ },
+ "&.Mui-focused fieldset": {
+ borderColor:
+ flagStatus[index]
+ .correct ===
+ true
+ ? "#4CAF50"
+ : flagStatus[
+ index
+ ]
+ .correct ===
+ false
+ ? "#F44336"
+ : "#A315D6"
+ }
+ },
+ "& .MuiInputBase-input": {
+ color: "white",
+ fontFamily: "Montserrat"
+ }
+ }}
+ />
+
+
+ ))}
+
+
+
+ Level 2
+
+
+ {flagInputs.slice(3, 6).map((flag, idx) => {
+ const index = idx + 3;
+ return (
+
+
+
+
+ FLAG {index + 1}
+ {flagStatus[index]
+ .correct === true &&
+ " (correct)"}
+ {flagStatus[index]
+ .correct === false &&
+ " (incorrect)"}
+
+
+ {getFlagPoints(index)}pts
+
+
+
+ handleFlagChange(
+ index,
+ e.target.value
+ )
+ }
+ placeholder="Enter flag..."
+ sx={{
+ fontFamily: "Montserrat",
+ "& .MuiOutlinedInput-root":
+ {
+ fontFamily:
+ "Montserrat",
+ color: "white",
+ backgroundColor:
+ "rgba(0, 0, 0, 0.3)",
+ borderRadius: "8px",
+ "& fieldset": {
+ borderColor:
+ flagStatus[
+ index
+ ]
+ .correct ===
+ true
+ ? "#4CAF50"
+ : flagStatus[
+ index
+ ]
+ .correct ===
+ false
+ ? "#F44336"
+ : "rgba(255, 255, 255, 0.3)"
+ },
+ "&:hover fieldset":
+ {
+ borderColor:
+ flagStatus[
+ index
+ ]
+ .correct ===
+ true
+ ? "#4CAF50"
+ : flagStatus[
+ index
+ ]
+ .correct ===
+ false
+ ? "#F44336"
+ : "rgba(163, 21, 214, 0.5)"
+ },
+ "&.Mui-focused fieldset":
+ {
+ borderColor:
+ flagStatus[
+ index
+ ]
+ .correct ===
+ true
+ ? "#4CAF50"
+ : flagStatus[
+ index
+ ]
+ .correct ===
+ false
+ ? "#F44336"
+ : "#A315D6"
+ }
+ },
+ "& .MuiInputBase-input": {
+ color: "white",
+ fontFamily: "Montserrat"
+ }
+ }}
+ />
+
+
+ );
+ })}
+
+
+
+ Level 3
+
+
+ {flagInputs.slice(6, 9).map((flag, idx) => {
+ const index = idx + 6;
+ return (
+
+
+
+
+ FLAG {index + 1}
+ {flagStatus[index]
+ .correct === true &&
+ " (correct)"}
+ {flagStatus[index]
+ .correct === false &&
+ " (incorrect)"}
+
+
+ {getFlagPoints(index)}pts
+
+
+
+ handleFlagChange(
+ index,
+ e.target.value
+ )
+ }
+ placeholder="Enter flag..."
+ sx={{
+ fontFamily: "Montserrat",
+ "& .MuiOutlinedInput-root":
+ {
+ fontFamily:
+ "Montserrat",
+ color: "white",
+ backgroundColor:
+ "rgba(0, 0, 0, 0.3)",
+ borderRadius: "8px",
+ "& fieldset": {
+ borderColor:
+ flagStatus[
+ index
+ ]
+ .correct ===
+ true
+ ? "#4CAF50"
+ : flagStatus[
+ index
+ ]
+ .correct ===
+ false
+ ? "#F44336"
+ : "rgba(255, 255, 255, 0.3)"
+ },
+ "&:hover fieldset":
+ {
+ borderColor:
+ flagStatus[
+ index
+ ]
+ .correct ===
+ true
+ ? "#4CAF50"
+ : flagStatus[
+ index
+ ]
+ .correct ===
+ false
+ ? "#F44336"
+ : "rgba(163, 21, 214, 0.5)"
+ },
+ "&.Mui-focused fieldset":
+ {
+ borderColor:
+ flagStatus[
+ index
+ ]
+ .correct ===
+ true
+ ? "#4CAF50"
+ : flagStatus[
+ index
+ ]
+ .correct ===
+ false
+ ? "#F44336"
+ : "#A315D6"
+ }
+ },
+ "& .MuiInputBase-input": {
+ color: "white",
+ fontFamily: "Montserrat"
+ }
+ }}
+ />
+
+
+ );
+ })}
+
+
+
+
+
+
+
+ ← Back to CTF
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/ctf/submit/submit.module.scss b/app/ctf/submit/submit.module.scss
new file mode 100644
index 00000000..b09804eb
--- /dev/null
+++ b/app/ctf/submit/submit.module.scss
@@ -0,0 +1,55 @@
+.ctfSubmitSection {
+ position: relative;
+ width: 100%;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background: linear-gradient(to bottom, #020316, #16133e);
+ z-index: 1;
+}
+
+@keyframes shimmer {
+ 0% {
+ background-position: 200% 0;
+ }
+ 100% {
+ background-position: -200% 0;
+ }
+}
+
+.flagCard {
+ background: rgba(255, 255, 255, 0.08);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-radius: 16px;
+ padding: 20px;
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 12px 35px rgba(163, 21, 214, 0.25);
+ border-color: rgba(163, 21, 214, 0.4);
+ }
+}
+
+.progressBar {
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 10px;
+ overflow: hidden;
+ height: 12px;
+
+ .progressFill {
+ height: 100%;
+ background: linear-gradient(90deg, #a315d6, #fdab60, #a315d6);
+ background-size: 200% 100%;
+ animation: shimmer 2s linear infinite;
+ transition: width 0.5s ease;
+ }
+}
+
+@media (max-width: 768px) {
+ .flagCard {
+ padding: 16px;
+ }
+}
diff --git a/app/ctf/utils.ts b/app/ctf/utils.ts
new file mode 100644
index 00000000..7508f747
--- /dev/null
+++ b/app/ctf/utils.ts
@@ -0,0 +1,4 @@
+export function derive(split: string[]) {
+ const decoded = split.map(c => atob(c));
+ return decoded.reverse().join("");
+}