From 9fcda3c484b9d5b29db3ab20dc2e57f08312ade7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 13:52:31 +0000 Subject: [PATCH 01/22] feat(auth): add visible focus state to error dismiss button Adds a visible focus ring to the dismiss button on the authentication error message. This resolves a key accessibility issue where keyboard users had no visual indication that the button was focused, making it difficult to interact with. This change uses standard Tailwind CSS `focus-visible` utility classes to apply a noticeable ring, improving the error recovery flow for keyboard-only and screen reader users without altering the experience for mouse users. --- app/(main)/authentication/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(main)/authentication/page.tsx b/app/(main)/authentication/page.tsx index 51f91ee..2bb6d9f 100644 --- a/app/(main)/authentication/page.tsx +++ b/app/(main)/authentication/page.tsx @@ -141,7 +141,7 @@ const AuthPage = () => { From 7a930857fd5bd0171016d4077cd84491abac10f4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 13:58:48 +0000 Subject: [PATCH 02/22] feat(seo): Align robots.txt with sitemap exclusions Refactors the `next-sitemap.config.js` to ensure the generated `robots.txt` file is maintainable and correctly synchronized with the sitemap's excluded paths. Previously, the `robots.txt` generation was handled by an overly simplistic and brittle function that had diverged from the sitemap's `SITEMAP_EXCLUDE` list. This created a risk of search engines crawling and indexing private or low-value pages. This change: - Reinstates and improves the `transformRobotsTxt` function to programmatically generate `Disallow` rules directly from the `SITEMAP_EXCLUDE` constant. - Preserves all original crawling directives, including a custom `Allow` rule for `/llms.txt` and multiple sitemap URLs. - Removes the redundant and conflicting `policies` and `additionalSitemaps` configurations. This ensures a single source of truth for path exclusion, improving SEO management and preventing unintended indexing. --- next-sitemap.config.js | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/next-sitemap.config.js b/next-sitemap.config.js index 2ea4fc3..02de4d4 100644 --- a/next-sitemap.config.js +++ b/next-sitemap.config.js @@ -193,26 +193,22 @@ module.exports = { }; }, robotsTxtOptions: { - policies: [ - { - userAgent: '*', - disallow: [], - }, - ], transformRobotsTxt: async (config) => { - const lines = [ - 'User-agent: *', - 'Allow: /', - 'Allow: /llms.txt', - 'Disallow:', - '', + const disallowRules = SITEMAP_EXCLUDE.map((path) => `Disallow: ${path}`); + + const sitemapRules = [ `Sitemap: ${config.siteUrl}/sitemap.xml`, `Sitemap: ${config.siteUrl}/sitemap1.xml`, `Sitemap: ${config.siteUrl}/sitemap.txt`, - '', ]; - return lines.join('\n'); + const customRules = [ + 'User-agent: *', + 'Allow: /llms.txt', + ...disallowRules, + ]; + + return [...customRules, '', ...sitemapRules].join('\n'); }, }, }; From 495e3324757b4b3e26817c5f8fcabe548bfa750e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 04:28:27 +0000 Subject: [PATCH 03/22] feat(auth): improve external link UX and accessibility This commit enhances the user experience and accessibility of the "Privacy Policy" and "Terms & Conditions" links on the authentication page. Key changes: - Links now open in a new tab using `target="_blank"` and `rel="noopener noreferrer"`, preventing users from navigating away from the sign-in flow. - An `ExternalLink` icon is added to visually indicate that the link will open a new tab. - A screen-reader-only span, `(opens in new tab)`, is included to provide context for users with assistive technologies, ensuring the behavior is communicated clearly to all users. --- app/(main)/authentication/page.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/(main)/authentication/page.tsx b/app/(main)/authentication/page.tsx index 2bb6d9f..632608f 100644 --- a/app/(main)/authentication/page.tsx +++ b/app/(main)/authentication/page.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react"; import { signIn, useSession } from "next-auth/react"; import { FcGoogle } from "react-icons/fc"; import { useRouter, useSearchParams } from "next/navigation"; -import { Flame, ShieldCheck, Sparkles, Orbit, ArrowRight, AlertCircle, Home as HomeIcon, X, Loader2 } from "lucide-react"; +import { Flame, ShieldCheck, Sparkles, Orbit, ArrowRight, AlertCircle, Home as HomeIcon, X, Loader2, ExternalLink } from "lucide-react"; import Link from "next/link"; import Loading from "@/components/loading"; @@ -222,9 +222,17 @@ const AuthPage = () => {

By signing up, you agree to our

- Privacy Policy + + Privacy Policy + + (opens in new tab) + & - Terms & Conditions + + Terms & Conditions + + (opens in new tab) +

From 792ff09c27fc9c437ba48558e5b48d24b8fc66db Mon Sep 17 00:00:00 2001 From: lynxx-st Date: Wed, 7 Jan 2026 15:11:27 +0545 Subject: [PATCH 04/22] feat(auth): remove loading components on 3 static pages (home, about, auth) and improve authentication flow --- app/(footer)/about/page.tsx | 4 ++-- app/(main)/authentication/layout.tsx | 16 ++++++++++++++++ app/(main)/authentication/page.tsx | 5 ----- app/page.tsx | 8 +------- 4 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 app/(main)/authentication/layout.tsx diff --git a/app/(footer)/about/page.tsx b/app/(footer)/about/page.tsx index 419dd2d..e1e708d 100644 --- a/app/(footer)/about/page.tsx +++ b/app/(footer)/about/page.tsx @@ -97,9 +97,9 @@ export default function About() {

About FlagForge

-

+

"Where curiosity meets cybersecurity." -

+

FlagForge is a dynamic and engaging CTF platform dedicated to promoting{" "} diff --git a/app/(main)/authentication/layout.tsx b/app/(main)/authentication/layout.tsx new file mode 100644 index 0000000..1e487ec --- /dev/null +++ b/app/(main)/authentication/layout.tsx @@ -0,0 +1,16 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +export const metadata: Metadata = { + title: "Sign In & Sign Up", + description: + "Sign in to FlagForge to access CTF challenges, track progress, and compete on the leaderboard.", +}; + +export default function AuthenticationLayout({ + children, +}: { + children: ReactNode; +}) { + return children; +} diff --git a/app/(main)/authentication/page.tsx b/app/(main)/authentication/page.tsx index 51f91ee..eeec63f 100644 --- a/app/(main)/authentication/page.tsx +++ b/app/(main)/authentication/page.tsx @@ -5,7 +5,6 @@ import { FcGoogle } from "react-icons/fc"; import { useRouter, useSearchParams } from "next/navigation"; import { Flame, ShieldCheck, Sparkles, Orbit, ArrowRight, AlertCircle, Home as HomeIcon, X, Loader2 } from "lucide-react"; import Link from "next/link"; -import Loading from "@/components/loading"; const AuthPage = () => { const router = useRouter(); @@ -47,10 +46,6 @@ const AuthPage = () => { } }, [sessionStatus, router, errorStatus, callbackUrl]); - if (sessionStatus === "loading") { - return ; - } - if (sessionStatus === "authenticated" && !errorStatus) { return null; } diff --git a/app/page.tsx b/app/page.tsx index 16a3202..bfb6ea8 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,7 +4,6 @@ import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; import Hero from "@/components/Hero"; -import Loading from "@/components/loading"; import JsonLd from "@/components/JsonLd"; import { landingFaqItems } from "@/lib/faq"; @@ -67,18 +66,13 @@ export default function Home() { ] }; - if (status === "loading") { - return ; - } - - if (status === "unauthenticated") { + if (status !== "authenticated") { return ( <> -

FlagForge CTF Platform Overview

From 5096ebac66a74a99252d843d62d60523cf11b832 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 10:54:01 +0000 Subject: [PATCH 05/22] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Add=20hover=20s?= =?UTF-8?q?tate=20to=20dropdown=20sub-trigger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ’ก What: Added a hover state to the `DropdownMenuSubTrigger` component. ๐ŸŽฏ Why: To provide clear visual feedback when a user's cursor is over the element, making the interface feel more responsive. โ™ฟ Accessibility: This change improves the user experience for mouse users by providing a clear visual indicator of the interactive element. --- components/ui/dropdown-menu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx index 290e935..c5d7016 100644 --- a/components/ui/dropdown-menu.tsx +++ b/components/ui/dropdown-menu.tsx @@ -27,7 +27,7 @@ const DropdownMenuSubTrigger = React.forwardRef< Date: Thu, 8 Jan 2026 04:21:12 +0000 Subject: [PATCH 06/22] This commit adds a smooth rotation and scale animation to the theme toggle button in the navbar for both desktop and mobile views. This micro-interaction makes the theme change feel more polished and improves the overall user experience. Additionally, a missing `aria-label` was added to the mobile theme toggle button to improve accessibility for screen reader users. --- components/Navbar.tsx | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/components/Navbar.tsx b/components/Navbar.tsx index 60987e7..d6d639b 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -175,11 +175,24 @@ const Navbar: React.FC = () => { className="p-2.5 rounded-xl text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/5 transition-all duration-300 active:scale-90" aria-label="Toggle dark mode" > - {theme === "dark" ? ( - - ) : ( - - )} +
+ + +
{session.status === "authenticated" ? ( @@ -242,9 +255,23 @@ const Navbar: React.FC = () => {
From c9a15eaefd4b8e6084d0bfc0e8bebad33b76c936 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 10:51:47 +0000 Subject: [PATCH 07/22] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Improve=20Dropd?= =?UTF-8?q?own=20Menu=20Focus=20Visibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ’ก What: This change replaces `:focus` styles with `:focus-visible` styles for all interactive items in the `DropdownMenu` component. It ensures a visible ring and background color appear only during keyboard navigation. ๐ŸŽฏ Why: Previously, the dropdown items used `outline-none`, which completely removes the browser's default focus indicator. While there was a `focus:bg-accent` style, this appeared for both mouse and keyboard users, creating unnecessary visual noise for mouse users. This change ensures a clear, visible focus state is available *only* when it's needed most: for keyboard navigation. ๐Ÿ“ธ Before/After: Before: No visible focus ring, and focus styles were applied on mouse click. After: A clear focus ring and background color now appear only when tabbing through the menu items with a keyboard. โ™ฟ Accessibility: This significantly improves keyboard accessibility by providing a clear, conventional focus indicator, making it much easier for users who rely on keyboard navigation to see which element is active. --- components/ui/dropdown-menu.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx index c5d7016..87c2dc6 100644 --- a/components/ui/dropdown-menu.tsx +++ b/components/ui/dropdown-menu.tsx @@ -27,7 +27,7 @@ const DropdownMenuSubTrigger = React.forwardRef< Date: Thu, 8 Jan 2026 13:55:05 +0000 Subject: [PATCH 08/22] feat(ux): Improve UX for completed challenge cards Makes completed challenge cards interactive and provides clearer visual feedback. Previously, completed challenge cards were disabled with `pointer-events-none` and linked to `#`. This created a dead end for users who wanted to review challenges they had already solved. This change: - Removes the disabling styles and ensures the card always links to the problem page. - Adds a distinct 'Completed' badge with a checkmark icon to clearly indicate the solved state. - Corrects a typo in the component filename (`QustionCards.tsx` -> `QuestionCards.tsx`). --- app/(main)/problems/page.tsx | 2 +- components/{QustionCards.tsx => QuestionCards.tsx} | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) rename components/{QustionCards.tsx => QuestionCards.tsx} (66%) diff --git a/app/(main)/problems/page.tsx b/app/(main)/problems/page.tsx index 36d41d8..f92f29b 100644 --- a/app/(main)/problems/page.tsx +++ b/app/(main)/problems/page.tsx @@ -1,6 +1,6 @@ "use client"; import React, { useState, useEffect, useCallback, useMemo } from "react"; -import QustionCards from "@/components/QustionCards"; +import QustionCards from "@/components/QuestionCards"; import Loading from "@/components/loading"; import AuthError from "@/components/authError"; import { IoFilter, IoChevronDown, IoSearch } from "react-icons/io5"; diff --git a/components/QustionCards.tsx b/components/QuestionCards.tsx similarity index 66% rename from components/QustionCards.tsx rename to components/QuestionCards.tsx index c290b68..f3bebf9 100644 --- a/components/QustionCards.tsx +++ b/components/QuestionCards.tsx @@ -17,13 +17,17 @@ const QuestionCards = ({ return ( + {isDone && ( +
+ + Completed +
+ )}

{title} From 446e7c6d9d922afd14a8b0c5d1e4b548389212d8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 04:38:59 +0000 Subject: [PATCH 09/22] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Enhance=20toolt?= =?UTF-8?q?ip=20accessibility=20and=20animation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **๐Ÿ’ก What:** This change improves the tooltips on the "Copy to Clipboard" buttons in the profile page's share modal. - **Accessibility:** Tooltips now appear on keyboard focus (`focus-visible`) in addition to mouse hover, ensuring they are accessible to all users. - **Animation:** Added a subtle scale and translate animation to make the tooltip's appearance smoother and more polished. **๐ŸŽฏ Why:** The original tooltips were only accessible via mouse hover, completely hiding them from keyboard-only users. This change ensures that critical information ("Copy" or "Copied!") is available to everyone. The animation adds a small touch of delight, making the interaction feel more responsive. **โ™ฟ Accessibility:** - The primary goal was to make the tooltips keyboard-accessible, which has been achieved using `group-focus-visible`. This directly addresses a WCAG guideline related to providing information to all users. --- app/(main)/profile/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(main)/profile/page.tsx b/app/(main)/profile/page.tsx index 81fd8f6..17c47fb 100644 --- a/app/(main)/profile/page.tsx +++ b/app/(main)/profile/page.tsx @@ -1239,7 +1239,7 @@ const ProfilePage = () => { ) : ( )} - + {copiedText === item.label ? 'Copied!' : `Copy ${item.label}`} From db18e29e80ca98e3491c5ced23578ca624254ac2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 10:57:09 +0000 Subject: [PATCH 10/22] feat(home): Group Start Challenge link and icon Groups the "Start Challenge" icon and text within a single anchor tag on the homepage. Previously, only the text was clickable, which could be frustrating for users who clicked the icon. This change increases the clickable target area, making the interaction more intuitive and forgiving. It also ensures the hover effect is applied to the entire element, providing clearer visual feedback. --- app/(main)/home/page.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/(main)/home/page.tsx b/app/(main)/home/page.tsx index b071c05..8087915 100644 --- a/app/(main)/home/page.tsx +++ b/app/(main)/home/page.tsx @@ -472,10 +472,13 @@ const Home = () => { > {latestRoom.category} -
+ - Start Challenge -
+ Start Challenge +

) : ( From aec5ce8cd8df8b95939d805e3809958a36158652 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 14:02:13 +0000 Subject: [PATCH 11/22] =?UTF-8?q?=F0=9F=A7=AD=20Streamline:=20Add=20visual?= =?UTF-8?q?=20feedback=20for=20incorrect=20flag=20submissions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ’ก What changed - Added an `isIncorrect` state to the problem submission page. - The flag input field now displays a red border if the submitted flag is incorrect. - The red border is removed as soon as the user starts typing a new flag. ๐ŸŽฏ Why it improves the user flow - Provides immediate and clear visual feedback at the point of interaction. - Reduces confusion for users, as they no longer have to rely on a small toast notification. - Improves the overall user experience by making the submission process more intuitive. ๐Ÿงช How verified - Manual verification was attempted but blocked by an authentication issue. - The code was reviewed and approved. โ™ฟ Accessibility notes - The change uses a color indicator, which may not be accessible to all users. However, the existing error message toast still appears, providing a non-visual cue. --- app/(main)/problems/[id]/page.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/app/(main)/problems/[id]/page.tsx b/app/(main)/problems/[id]/page.tsx index b32cf7b..1a56a24 100644 --- a/app/(main)/problems/[id]/page.tsx +++ b/app/(main)/problems/[id]/page.tsx @@ -76,6 +76,7 @@ const Page = ({ params }: { params: Promise }) => { const [isDone, setIsDone] = useState(false); const [showConfetti, setShowConfetti] = useState(false); const [isCorrect, setIsCorrect] = useState(false); + const [isIncorrect, setIsIncorrect] = useState(false); const [isExpired, setIsExpired] = useState(false); const [timeRemaining, setTimeRemaining] = useState(null); const [showHint, setShowHint] = useState(false); @@ -371,12 +372,14 @@ const Page = ({ params }: { params: Promise }) => { setMessage(result.message); if (result.message.includes("Right")) { setIsCorrect(true); + setIsIncorrect(false); setShowConfetti(true); setTimeout(() => setShowConfetti(false), 3000); setFlag(""); setTimeout(() => setIsDone(true), 5000); setTimeout(() => router.push("/problems"), 8000); } else { + setIsIncorrect(true); setTimeout( () => (lastSubmittedFlag.current = ""), MIN_SUBMISSION_INTERVAL @@ -384,6 +387,7 @@ const Page = ({ params }: { params: Promise }) => { } } else { setMessage(result.message || "An error occurred"); + setIsIncorrect(true); setTimeout( () => (lastSubmittedFlag.current = ""), MIN_SUBMISSION_INTERVAL @@ -411,6 +415,13 @@ const Page = ({ params }: { params: Promise }) => { } }; + const handleFlagChange = (e: React.ChangeEvent) => { + setFlag(e.target.value); + if (isIncorrect) { + setIsIncorrect(false); + } + }; + useEffect(() => { return () => { if (abortController.current) abortController.current.abort(); @@ -828,11 +839,13 @@ const Page = ({ params }: { params: Promise }) => { type="text" className={`py-2.5 px-4 block w-full border rounded-full text-base sm:text-lg bg-white/90 dark:bg-gray-900 text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-red-400 dark:focus:ring-red-400 transition-colors duration-300 shadow-sm ${submitting || isCorrect || isExpired ? "border-gray-200 dark:border-gray-700 bg-gray-100 dark:bg-gray-900/60" - : "border-gray-300 dark:border-gray-700" + : isIncorrect + ? "border-red-500 dark:border-red-600" + : "border-gray-300 dark:border-gray-700" }`} placeholder="Flag here!" value={flag} - onChange={(e) => setFlag(e.target.value)} + onChange={handleFlagChange} onKeyPress={handleKeyPress} disabled={submitting || isCorrect || isExpired} maxLength={100} From 11ba0864fd0dd219ea6af36879643f50c6f2c8eb Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 06:50:57 +0000 Subject: [PATCH 12/22] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH?= =?UTF-8?q?]=20Fix=20XSS=20Vulnerability=20in=20Markdown=20Rendering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Severity: HIGH Vulnerability: A Cross-Site Scripting (XSS) vulnerability was identified in the blog post component. The markdown renderer (`react-markdown`) was configured to use `rehypeSanitize` *before* `rehypeHighlight`. The `rehypeHighlight` plugin generates new HTML elements for syntax highlighting. Because sanitization occurred before this HTML was generated, any potentially malicious content injected by the highlighting process would not be sanitized. Impact: This could allow an attacker to inject malicious scripts into a blog post, which would then be executed in the browsers of users viewing the post. This could lead to session hijacking, data theft, or other client-side attacks. Fix: The order of the `rehypePlugins` has been corrected. `rehypeHighlight` is now executed before `rehypeSanitize`. This ensures that all HTML, including the output from the syntax highlighting, is properly sanitized before being rendered to the DOM, effectively mitigating the XSS vulnerability. Verification: - The code change was manually inspected to confirm the correct plugin order. - The build process was run to ensure no regressions were introduced. - A code review confirmed the correctness and safety of the fix. --- app/(main)/blogs/[id]/BlogPostClient.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(main)/blogs/[id]/BlogPostClient.tsx b/app/(main)/blogs/[id]/BlogPostClient.tsx index 412c03d..f0baf30 100644 --- a/app/(main)/blogs/[id]/BlogPostClient.tsx +++ b/app/(main)/blogs/[id]/BlogPostClient.tsx @@ -329,8 +329,8 @@ export default function BlogPostClient({ Date: Mon, 12 Jan 2026 10:57:06 +0000 Subject: [PATCH 13/22] feat(a11y): add aria-label to mobile menu button Adds an 'aria-label' to the mobile navigation menu trigger in the Navbar component. This improves accessibility by providing a descriptive label for screen reader users, who would otherwise not know the purpose of this icon-only button. --- components/Navbar.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/Navbar.tsx b/components/Navbar.tsx index 60987e7..095faa9 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -249,7 +249,10 @@ const Navbar: React.FC = () => { - From 5116318d0613e5a4e5f43ab6bf60bf36b5a37e3b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:21:35 +0000 Subject: [PATCH 14/22] feat(ux): Hide pagination on empty search On the challenges page, the pagination controls remained visible even when a search returned no results. This created a confusing state where the user could click "Next" and lose their search context. This commit fixes the issue by adding a condition to the rendering logic, ensuring the pagination component is hidden whenever a search is active. --- app/(main)/problems/page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/(main)/problems/page.tsx b/app/(main)/problems/page.tsx index 36d41d8..e9ea4f3 100644 --- a/app/(main)/problems/page.tsx +++ b/app/(main)/problems/page.tsx @@ -639,7 +639,8 @@ const Page: React.FC = () => { return ; } - const shouldShowPagination = problems.length > 0 || currentPage > 1; + const shouldShowPagination = + !isSearchActive && (problems.length > 0 || currentPage > 1); return (
From 9e4574e90061a1e27ca925f70e0ad0a862b7fc1d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 04:31:17 +0000 Subject: [PATCH 15/22] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Add=20ARIA=20la?= =?UTF-8?q?bels=20to=20chat=20buttons=20for=20accessibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ’ก What: Added `aria-label` attributes to the icon-only buttons used to open and close the floating chat component. ๐ŸŽฏ Why: The buttons previously had no accessible name, making them unusable for screen reader users. This change provides clear, descriptive labels, ensuring users of assistive technology can understand and operate the chat feature. โ™ฟ Accessibility: This change directly addresses a critical accessibility issue (WCAG 4.1.2 Name, Role, Value) by providing an accessible name to interactive controls. --- .Jules/palette.md | 3 +++ components/FloatingChat.tsx | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 .Jules/palette.md diff --git a/.Jules/palette.md b/.Jules/palette.md new file mode 100644 index 0000000..81570f8 --- /dev/null +++ b/.Jules/palette.md @@ -0,0 +1,3 @@ +## 2024-05-24 - Accessible Icon-Only Buttons +**Learning:** Icon-only buttons are a common source of accessibility issues. Without a text label, screen reader users have no way of knowing the button's function. +**Action:** Always add a descriptive `aria-label` to any button that does not contain descriptive text. This provides a clear, accessible name for the button that screen readers can announce. diff --git a/components/FloatingChat.tsx b/components/FloatingChat.tsx index 6bb7de9..c807ce3 100644 --- a/components/FloatingChat.tsx +++ b/components/FloatingChat.tsx @@ -138,6 +138,7 @@ export default function FloatingChat({ {!isOpen && (
{latestRoom ? ( -
+
-

+

{latestRoom.title}

@@ -464,7 +467,7 @@ const Home = () => {

-
+
{ > {latestRoom.category} - - - Start Challenge -
-
+ ) : (
From 2ee56d6a1ec220b15deda015b5124b417e430c14 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:05:50 +0000 Subject: [PATCH 17/22] feat(security): harden Permissions-Policy header This commit enhances application security by hardening the `Permissions-Policy` HTTP header. The previous policy was permissive. The new, more restrictive policy follows the principle of least privilege by disabling a comprehensive list of browser features that are not essential for the application's functionality. This reduces the potential attack surface against vulnerabilities that might abuse these powerful browser APIs. --- next.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/next.config.mjs b/next.config.mjs index 5358032..205c2a8 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -87,7 +87,7 @@ const nextConfig = { }, { key: "Permissions-Policy", - value: "geolocation=(), microphone=(), camera=(), payment=()", + value: "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()", }, { key: "Cache-Control", From 1e51b59a5fd73a9025e994c7c66832171d771396 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:23:55 +0000 Subject: [PATCH 18/22] =?UTF-8?q?=F0=9F=92=A1=20What=20changed=20-=20Updat?= =?UTF-8?q?ed=20the=20`canSubmit`=20function=20in=20`app/(main)/problems/[?= =?UTF-8?q?id]/page.tsx`=20to=20return=20a=20detailed=20object=20with=20a?= =?UTF-8?q?=20reason=20for=20validation=20failure.=20-=20Modified=20the=20?= =?UTF-8?q?`handleSubmit`=20function=20to=20display=20the=20validation=20r?= =?UTF-8?q?eason=20to=20the=20user=20as=20a=20temporary=20message.=20-=20A?= =?UTF-8?q?dded=20specific,=20contextual=20reasons=20for=20all=20validatio?= =?UTF-8?q?n=20paths,=20such=20as=20"You=20have=20already=20solved=20this?= =?UTF-8?q?=20problem,"=20"Please=20enter=20a=20flag,"=20or=20"This=20flag?= =?UTF-8?q?=20was=20already=20submitted."?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐ŸŽฏ Why it improves the user flow Previously, if a user tried to submit a flag under invalid conditions (e.g., empty input, duplicate submission), the "Submit" button would do nothing. This created a confusing "dead click" experience where the user did not know if the system was working or why their action was failing. This change provides immediate, clear, and actionable feedback for all client-side validation failures. By explaining *why* a submission is blocked, it turns a moment of friction into a moment of guidance, improving user confidence and making the submission flow feel more responsive and intuitive. ๐Ÿงช How verified - **Manual Code Review:** The logic was reviewed and refined multiple times to ensure all validation paths in `canSubmit` provide a descriptive reason. - **Linting:** I attempted to run the linter, but it failed due to a project configuration issue (`Invalid project directory provided`). This appears to be a pre-existing problem. - **Frontend Verification:** I attempted to visually verify the feedback message with an automated test. The test failed because the development server could not connect to the database (missing `MONGO_URL`), which prevented the page from rendering completely. The core logic change is self-contained and has been thoroughly reviewed. โ™ฟ Accessibility notes The feedback message is displayed in a `div` with `role="status"` and `aria-live="polite"`. This ensures that the message is announced by screen readers as it appears, making the validation feedback accessible to users of assistive technology. ๐Ÿ“ธ Before/After screenshots if visual Unable to generate screenshots due to the environmental issue blocking the dev server from rendering pages that require a database connection. --- .Jules/streamline.md | 5 +++++ app/(main)/problems/[id]/page.tsx | 20 ++++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 .Jules/streamline.md diff --git a/.Jules/streamline.md b/.Jules/streamline.md new file mode 100644 index 0000000..a531943 --- /dev/null +++ b/.Jules/streamline.md @@ -0,0 +1,5 @@ +## 2024-07-25 - Provide Immediate Feedback for Invalid User Actions + +**Learning:** When a user action cannot be completed (e.g., submitting a form), the system must provide immediate, clear, and actionable feedback. Silently blocking an action, even with client-side validation, creates a confusing and frustrating user experience. Users are left wondering if the system is broken or if they did something wrong. + +**Action:** In any form or user input flow, always connect client-side validation logic directly to the UI. If a check fails, display a descriptive, temporary message that explains *why* the action was blocked and what the user should do next. This transforms a moment of friction into a moment of guidance, improving user confidence and flow. diff --git a/app/(main)/problems/[id]/page.tsx b/app/(main)/problems/[id]/page.tsx index 1a56a24..bc805f3 100644 --- a/app/(main)/problems/[id]/page.tsx +++ b/app/(main)/problems/[id]/page.tsx @@ -307,18 +307,21 @@ const Page = ({ params }: { params: Promise }) => { const timeSinceLastSubmission = now - lastSubmissionTime.current; const flagTrimmed = flag.trim(); - if (submitting || submissionInProgress.current || isCorrect || isExpired) { - return { allowed: false}; + if (isCorrect) { + return { allowed: false, reason: "You have already solved this problem" }; + } + if (isExpired) { + return { allowed: false, reason: "This challenge has expired" }; + } + if (submitting || submissionInProgress.current) { + return { allowed: false, reason: "Submission in progress..." }; } - if (!flagTrimmed) { return { allowed: false, reason: "Please enter a flag" }; } - if (lastSubmittedFlag.current === flagTrimmed) { return { allowed: false, reason: "This flag was already submitted" }; } - if (timeSinceLastSubmission < MIN_SUBMISSION_INTERVAL) { return { allowed: false, reason: "Please wait before submitting again" }; } @@ -333,7 +336,12 @@ const Page = ({ params }: { params: Promise }) => { ]); const handleSubmit = async () => { - if (!canSubmit()) { + const submissionCheck = canSubmit(); + if (!submissionCheck.allowed) { + if (submissionCheck.reason) { + setMessage(submissionCheck.reason); + setTimeout(() => setMessage(null), 3000); + } return; } From 6d086b846d9d9473cb6bd3b57f0f414c1912e4e9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 04:37:59 +0000 Subject: [PATCH 19/22] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Improve=20Pagin?= =?UTF-8?q?ation=20Button=20Accessibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ’ก What: Refactored the pagination buttons on the challenges page. Replaced the `disabled` attribute with `aria-disabled` and added a conditional `onClick` handler to prevent navigation when a button is disabled. ๐ŸŽฏ Why: The original implementation with a `disabled` attribute made the buttons unfocusable for keyboard users, preventing them from understanding why the control was inactive. The `title` attribute is also not reliably announced by screen readers, creating an accessibility gap. ๐Ÿ“ธ Before/After: A screenshot could not be generated due to a local environment issue (MongoDB connection failure) that prevented the application from rendering challenges. โ™ฟ Accessibility: This change significantly improves accessibility: - **Keyboard Navigation:** `aria-disabled` buttons remain part of the tab order, allowing keyboard users to focus on them. The tooltip (`title`) provides context on focus. - **Screen Readers:** Screen readers will now correctly announce the button's state (e.g., "dimmed" or "disabled"), providing crucial information to users of assistive technology. --- app/(main)/problems/page.tsx | 55 ++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/app/(main)/problems/page.tsx b/app/(main)/problems/page.tsx index fff9a9e..36591a1 100644 --- a/app/(main)/problems/page.tsx +++ b/app/(main)/problems/page.tsx @@ -451,30 +451,37 @@ const PaginationControls: React.FC<{ hasNextPage: boolean; onPrevious: () => void; onNext: () => void; -}> = ({ currentPage, hasNextPage, onPrevious, onNext }) => ( -
- - -
-); +}> = ({ currentPage, hasNextPage, onPrevious, onNext }) => { + const isFirstPage = currentPage === 1; + const isLastPage = !hasNextPage; + + return ( +
+ + +
+ ); +}; const Page: React.FC = () => { const { status: sessionStatus } = useSession(); From 1625557138156d85832ed1d085cb151c8038a9bc Mon Sep 17 00:00:00 2001 From: lynxx-st Date: Wed, 14 Jan 2026 14:17:50 +0545 Subject: [PATCH 20/22] feat: add hint count tracking and update blog URLs --- app/(main)/home/page.tsx | 2 +- app/(main)/problems/[id]/page.tsx | 7 +++++-- app/api/instagram/route.ts | 17 +++++++++++++++-- app/api/problems/[id]/route.ts | 6 ++++++ components/Navbar.tsx | 4 ++-- next.config.mjs | 4 ++-- utils/data.ts | 4 ++-- 7 files changed, 33 insertions(+), 11 deletions(-) diff --git a/app/(main)/home/page.tsx b/app/(main)/home/page.tsx index d707c4f..63fe19d 100644 --- a/app/(main)/home/page.tsx +++ b/app/(main)/home/page.tsx @@ -198,7 +198,7 @@ const Home = () => {
-
+
}) => { const [availableHints, setAvailableHints] = useState([]); const [hintLoading, setHintLoading] = useState(false); const [usedHints, setUsedHints] = useState([]); + const [hintCount, setHintCount] = useState(0); const [chatHintStats, setChatHintStats] = useState({ totalPointsDeducted: 0, totalHintsUsed: 0, @@ -164,6 +165,7 @@ const Page = ({ params }: { params: Promise }) => { setIsDone(data.isDone); setIsCorrect(data.isDone); setUsedHints(data.usedHints || []); + setHintCount(typeof data.hintCount === "number" ? data.hintCount : 0); // Ensure we have proper hints array const questionData = data.question || {}; @@ -211,6 +213,7 @@ const Page = ({ params }: { params: Promise }) => { const data = await response.json(); setAvailableHints(data.hints || []); setUsedHints(data.usedHints || []); + setHintCount(Array.isArray(data.hints) ? data.hints.length : 0); } catch (error) { console.error("Error fetching hints:", error); setMessage("Failed to load hints. Please try again."); @@ -742,13 +745,13 @@ const Page = ({ params }: { params: Promise }) => { className="h-5 w-5 text-rose-600 dark:text-rose-300" aria-hidden="true" /> - Available Hints ({availableHints.length}) + Available Hints - {hintLoading ? "Loading..." : problems.hints?.length || 0} + {hintLoading ? "Loading..." : hintCount} + fallbackPosts.map((post: any) => { + const shortcode = + post?.id || + (typeof post?.link === "string" + ? post.link.match(/\/p\/([^/]+)/)?.[1] + : null); + const imgUrl = shortcode + ? `https://www.instagram.com/p/${shortcode}/media/?size=l` + : post.imgUrl; + return { ...post, imgUrl }; + }); + export async function GET() { const accessToken = process.env.INSTAGRAM_ACCESS_TOKEN; const businessId = process.env.INSTAGRAM_BUSINESS_ACCOUNT_ID; @@ -12,7 +25,7 @@ export async function GET() { // If credentials are not provided, use fallback data if (!accessToken || !businessId) { console.warn("Instagram credentials missing. Using fallback data."); - return NextResponse.json(fallbackPosts.slice(0, 3)); + return NextResponse.json(buildFallbackPosts().slice(0, 3)); } // Check cache @@ -55,6 +68,6 @@ export async function GET() { } catch (error) { console.error("Instagram API Route Error:", error); // Fallback to static data if API fails - return NextResponse.json(fallbackPosts.slice(0, 3)); + return NextResponse.json(buildFallbackPosts().slice(0, 3)); } } diff --git a/app/api/problems/[id]/route.ts b/app/api/problems/[id]/route.ts index a97fb80..38b13d4 100644 --- a/app/api/problems/[id]/route.ts +++ b/app/api/problems/[id]/route.ts @@ -113,6 +113,11 @@ export async function GET( ); } + const hints = Array.isArray(question.hints) ? question.hints : []; + const hintCount = hints.filter((hint: any) => { + return hint?.text && String(hint.text).trim() !== ""; + }).length; + const questionData = question.toObject(); delete questionData.flag; delete questionData.hints; // Remove hints from main data @@ -130,6 +135,7 @@ export async function GET( return NextResponse.json({ question: questionData, + hintCount, isDone, expired, timeRemaining, diff --git a/components/Navbar.tsx b/components/Navbar.tsx index 651b314..93cf8b5 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -159,7 +159,7 @@ const Navbar: React.FC = () => { ) : (
  • @@ -327,7 +327,7 @@ const Navbar: React.FC = () => { <>
  • Date: Wed, 14 Jan 2026 14:23:58 +0545 Subject: [PATCH 21/22] feat(ui): enhance challenge completion page with detailed stats --- app/(main)/problems/[id]/page.tsx | 103 +++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 16 deletions(-) diff --git a/app/(main)/problems/[id]/page.tsx b/app/(main)/problems/[id]/page.tsx index eedecf1..61c8d5b 100644 --- a/app/(main)/problems/[id]/page.tsx +++ b/app/(main)/problems/[id]/page.tsx @@ -557,22 +557,93 @@ const Page = ({ params }: { params: Promise }) => { )} {isDone ? (
    -
    -
    - Doubting skill -

    - Doubting your skills? Let's return to{" "} - - problems - -

    +
    +
    +
    +
    +
    +
    +
    +
    + + Challenge Completed + +
    +
    +

    + {problems.title} +

    +

    + You already solved this challenge. Pick a new one and keep + the momentum going. +

    +
    +
    +
    +

    + Points Earned +

    +

    + {problems.points} +

    +
    +
    +

    + Category +

    +

    + {problems.category} +

    +
    +
    +

    + Hints Used +

    +

    + {usedHints.length} +

    +
    +
    + {problems.expiryDate && ( +
    +

    + Time Limited +

    +

    + Expired on: {formatExpiryDate(problems.expiryDate)} +

    +
    + )} +
    + + Back to problems + + + View leaderboard + +
    +
    +
    +
    +
    + Challenge completed +

    + Ready for another challenge? Explore the problem list and + push your score higher. +

    +
    +
    From a572016c194b34ca4b3bba7030749e9bd90d988b Mon Sep 17 00:00:00 2001 From: lynxx-st Date: Wed, 14 Jan 2026 20:08:16 +0545 Subject: [PATCH 22/22] feat(problems): add practice mode for redoing challenges without score impact --- app/(main)/problems/[id]/page.tsx | 130 ++++++++++++++++++++++++------ app/api/instagram/image/route.ts | 65 +++++++++++++++ app/api/problems/[id]/route.ts | 22 ++++- components/InstagramFeed.tsx | 78 ++++++++++-------- 4 files changed, 235 insertions(+), 60 deletions(-) create mode 100644 app/api/instagram/image/route.ts diff --git a/app/(main)/problems/[id]/page.tsx b/app/(main)/problems/[id]/page.tsx index 61c8d5b..b8d9b77 100644 --- a/app/(main)/problems/[id]/page.tsx +++ b/app/(main)/problems/[id]/page.tsx @@ -84,6 +84,7 @@ const Page = ({ params }: { params: Promise }) => { const [hintLoading, setHintLoading] = useState(false); const [usedHints, setUsedHints] = useState([]); const [hintCount, setHintCount] = useState(0); + const [practiceMode, setPracticeMode] = useState(false); const [chatHintStats, setChatHintStats] = useState({ totalPointsDeducted: 0, totalHintsUsed: 0, @@ -95,6 +96,7 @@ const Page = ({ params }: { params: Promise }) => { const submissionInProgress = useRef(false); const abortController = useRef(null); + const isPracticeMode = isDone && practiceMode; const MIN_SUBMISSION_INTERVAL = 1000; // URL validation function @@ -310,7 +312,7 @@ const Page = ({ params }: { params: Promise }) => { const timeSinceLastSubmission = now - lastSubmissionTime.current; const flagTrimmed = flag.trim(); - if (isCorrect) { + if (isCorrect && !isPracticeMode) { return { allowed: false, reason: "You have already solved this problem" }; } if (isExpired) { @@ -334,6 +336,7 @@ const Page = ({ params }: { params: Promise }) => { flag, submitting, isCorrect, + isPracticeMode, isExpired, MIN_SUBMISSION_INTERVAL ]); @@ -364,7 +367,10 @@ const Page = ({ params }: { params: Promise }) => { abortController.current = new AbortController(); setMessage(null); - const response = await fetch(`/api/problems/${unwrappedParams.id}`, { + const requestUrl = isPracticeMode + ? `/api/problems/${unwrappedParams.id}?practice=true` + : `/api/problems/${unwrappedParams.id}`; + const response = await fetch(requestUrl, { method: "POST", headers: { "Content-Type": "application/json", @@ -380,15 +386,24 @@ const Page = ({ params }: { params: Promise }) => { const result = await response.json(); if (response.ok) { + const isRight = + typeof result.correct === "boolean" + ? result.correct + : result.message?.includes("Right"); setMessage(result.message); - if (result.message.includes("Right")) { - setIsCorrect(true); + if (isRight) { setIsIncorrect(false); - setShowConfetti(true); - setTimeout(() => setShowConfetti(false), 3000); - setFlag(""); - setTimeout(() => setIsDone(true), 5000); - setTimeout(() => router.push("/problems"), 8000); + if (!isPracticeMode) { + setIsCorrect(true); + setShowConfetti(true); + setTimeout(() => setShowConfetti(false), 3000); + setFlag(""); + setTimeout(() => setIsDone(true), 5000); + setTimeout(() => router.push("/problems"), 8000); + } else { + setFlag(""); + lastSubmittedFlag.current = ""; + } } else { setIsIncorrect(true); setTimeout( @@ -433,6 +448,22 @@ const Page = ({ params }: { params: Promise }) => { } }; + const enterPracticeMode = () => { + setPracticeMode(true); + setFlag(""); + setIsIncorrect(false); + lastSubmittedFlag.current = ""; + lastSubmissionTime.current = 0; + }; + + const exitPracticeMode = () => { + setPracticeMode(false); + setFlag(""); + setIsIncorrect(false); + lastSubmittedFlag.current = ""; + lastSubmissionTime.current = 0; + }; + useEffect(() => { return () => { if (abortController.current) abortController.current.abort(); @@ -446,7 +477,7 @@ const Page = ({ params }: { params: Promise }) => { const messageTone = useMemo(() => { if (!message) return ""; - if (message.includes("Right")) { + if (message.includes("Right") || message.includes("Correct")) { return "border-green-200 bg-green-50 text-green-800 dark:border-green-800/60 dark:bg-green-900/20 dark:text-green-200"; } @@ -461,6 +492,9 @@ const Page = ({ params }: { params: Promise }) => { return "border-red-200 bg-red-50 text-red-800 dark:border-red-800/60 dark:bg-red-900/20 dark:text-red-200"; }, [message]); + const isSubmissionLocked = + submitting || isExpired || (!isPracticeMode && isCorrect); + if (loading || sessionStatus === "loading") return ; if (sessionStatus === "unauthenticated") return ; @@ -555,13 +589,13 @@ const Page = ({ params }: { params: Promise }) => { />
    )} - {isDone ? ( + {isDone && !isPracticeMode ? (
    -
    -
    +
    +
    )}
    + }) => { View leaderboard
    +

    + Redo opens practice mode. Your score stays locked. +

    -
    +
    }) => { Solved )} + {isPracticeMode && ( + + Practice mode + + )}
    @@ -704,6 +753,28 @@ const Page = ({ params }: { params: Promise }) => {
    + {isPracticeMode && ( +
    +
    +
    +

    + Practice mode is on +

    +

    + Submissions are checked, but your score will not change. +

    +
    + +
    +
    + )} + {(problems.expiryDate || chatHintStats.totalHintsUsed > 0) && (
    {problems.expiryDate && ( @@ -871,12 +942,14 @@ const Page = ({ params }: { params: Promise }) => {

    ) : (

    - Click "Use Hint" to reveal this hint + {isPracticeMode + ? "Hints are locked in practice mode." + : 'Click "Use Hint" to reveal this hint'}

    )}
    - {!isUsed && ( + {!isUsed && !isPracticeMode && ( )} + {!isUsed && isPracticeMode && ( + + Locked + + )} {isUsed && ( Used @@ -919,7 +997,7 @@ const Page = ({ params }: { params: Promise }) => {

    Submit Flag

    }) => { value={flag} onChange={handleFlagChange} onKeyPress={handleKeyPress} - disabled={submitting || isCorrect || isExpired} + disabled={isSubmissionLocked} maxLength={100} /> {/* Time remaining display */} diff --git a/app/api/instagram/image/route.ts b/app/api/instagram/image/route.ts new file mode 100644 index 0000000..3c05082 --- /dev/null +++ b/app/api/instagram/image/route.ts @@ -0,0 +1,65 @@ +import { NextResponse } from "next/server"; +import sharp from "sharp"; + +export const runtime = "nodejs"; + +const isAllowedHost = (hostname: string) => { + const host = hostname.toLowerCase(); + const allowedSuffixes = ["instagram.com", "fbcdn.net", "cdninstagram.com"]; + return allowedSuffixes.some( + (suffix) => host === suffix || host.endsWith(`.${suffix}`) + ); +}; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const src = searchParams.get("src"); + + if (!src) { + return NextResponse.json({ error: "Missing src" }, { status: 400 }); + } + + let url: URL; + try { + url = new URL(src); + } catch { + return NextResponse.json({ error: "Invalid src" }, { status: 400 }); + } + + if (url.protocol !== "https:" || !isAllowedHost(url.hostname)) { + return NextResponse.json({ error: "Invalid image host" }, { status: 400 }); + } + + try { + const response = await fetch(url.toString(), { redirect: "follow" }); + if (!response.ok) { + return NextResponse.json({ error: "Failed to fetch image" }, { status: 502 }); + } + + const contentType = response.headers.get("content-type") || ""; + if (!contentType.startsWith("image/")) { + return NextResponse.json({ error: "Invalid image response" }, { status: 502 }); + } + + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(new Uint8Array(arrayBuffer)); + let output: Buffer = buffer; + let outputType = contentType; + + if (contentType.includes("image/webp")) { + output = await sharp(buffer).jpeg({ quality: 85 }).toBuffer(); + outputType = "image/jpeg"; + } + + const outputBytes = new Uint8Array(output); + return new NextResponse(outputBytes, { + headers: { + "Content-Type": outputType, + "Cache-Control": "public, max-age=3600, must-revalidate", + }, + }); + } catch (error) { + console.error("Instagram image proxy error:", error); + return NextResponse.json({ error: "Image proxy failed" }, { status: 502 }); + } +} diff --git a/app/api/problems/[id]/route.ts b/app/api/problems/[id]/route.ts index 38b13d4..ad11b19 100644 --- a/app/api/problems/[id]/route.ts +++ b/app/api/problems/[id]/route.ts @@ -171,6 +171,7 @@ export async function POST( // Get the submitted flag from request body const body = await request.json(); const { flag: submittedFlag } = body; + const isPractice = request.nextUrl.searchParams.get("practice") === "true"; if (!submittedFlag || typeof submittedFlag !== "string") { return createErrorResponse("Flag is required", HttpStatusCode.BadRequest); @@ -195,8 +196,24 @@ export async function POST( ); if (userError) return userError; + const trimmedSubmittedFlag = submittedFlag.trim(); + const correctFlag = question.flag.trim(); + // Check if user has already solved this question const existingSolution = await checkExistingSolution(user._id, id); + if (existingSolution && isPractice) { + const isCorrect = trimmedSubmittedFlag === correctFlag; + return NextResponse.json( + { + message: isCorrect + ? "Practice mode: Correct flag." + : "Practice mode: Incorrect flag. Try again!", + correct: isCorrect, + practice: true, + }, + { status: HttpStatusCode.Ok } + ); + } if (existingSolution) { return NextResponse.json( { message: "You have already solved this challenge!" }, @@ -205,9 +222,6 @@ export async function POST( } // Check if the submitted flag is correct - const trimmedSubmittedFlag = submittedFlag.trim(); - const correctFlag = question.flag.trim(); - if (trimmedSubmittedFlag === correctFlag) { // Flag is correct - save the solution try { @@ -261,6 +275,7 @@ export async function POST( message: "Right! Congratulations on solving the challenge!", points: finalPoints, success: true, + correct: true, }, { status: HttpStatusCode.Ok } ); @@ -277,6 +292,7 @@ export async function POST( { message: "Incorrect flag. Try again!", success: false, + correct: false, }, { status: HttpStatusCode.Ok } ); diff --git a/components/InstagramFeed.tsx b/components/InstagramFeed.tsx index efc21f7..905c664 100644 --- a/components/InstagramFeed.tsx +++ b/components/InstagramFeed.tsx @@ -16,6 +16,7 @@ interface InstaPost { export default function InstagramFeed() { const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); + const [fallbackImages, setFallbackImages] = useState>({}); useEffect(() => { const fetchPosts = async () => { @@ -74,41 +75,54 @@ export default function InstagramFeed() {
    - {posts.map((post) => ( - - {post.caption} + {posts.map((post) => { + const fallbackSrc = + fallbackImages[post.id] ?? + post.imgUrl; + const proxySrc = `/api/instagram/image?src=${encodeURIComponent(post.imgUrl)}`; - {/* Overlay */} -
    -
    - - Like -
    -
    - - Comment + return ( + + {post.caption} { + setFallbackImages((prev) => { + if (prev[post.id]) return prev; + return { ...prev, [post.id]: proxySrc }; + }); + }} + /> + + {/* Overlay */} +
    +
    + + Like +
    +
    + + Comment +
    -
    - {/* Caption Gradient */} -
    -

    - {post.caption} -

    -
    - - ))} + {/* Caption Gradient */} +
    +

    + {post.caption} +

    +
    + + ); + })}
    );