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/.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/(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..4b607cf 100644 --- a/app/(main)/authentication/page.tsx +++ b/app/(main)/authentication/page.tsx @@ -3,9 +3,8 @@ 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"; 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; } @@ -141,7 +136,7 @@ const AuthPage = () => { @@ -222,9 +217,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) +

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({ {
-
+
{
{latestRoom ? ( -
+
-

+

{latestRoom.title}

@@ -477,7 +481,7 @@ const Home = () => {

-
+
{ > {latestRoom.category} -
- - Start Challenge -
-
+ ) : (
@@ -610,6 +610,9 @@ const Home = () => { {/* Scoreboards Section */} + + {/* Instagram Feed */} +
diff --git a/app/(main)/problems/[id]/page.tsx b/app/(main)/problems/[id]/page.tsx index be353be..688a58c 100644 --- a/app/(main)/problems/[id]/page.tsx +++ b/app/(main)/problems/[id]/page.tsx @@ -78,12 +78,15 @@ 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); const [availableHints, setAvailableHints] = useState([]); 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 +98,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 @@ -165,6 +169,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 || {}; @@ -212,6 +217,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."); @@ -314,18 +320,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 && !isPracticeMode) { + 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" }; } @@ -335,9 +344,10 @@ const Page = ({ params }: { params: Promise }) => { flag, submitting, isCorrect, + isPracticeMode, isExpired, MIN_SUBMISSION_INTERVAL - ]); + ]); const handleSubmit = async () => { // Check authentication before allowing flag submission @@ -346,7 +356,12 @@ const Page = ({ params }: { params: Promise }) => { return; } - if (!canSubmit()) { + const submissionCheck = canSubmit(); + if (!submissionCheck.allowed) { + if (submissionCheck.reason) { + setMessage(submissionCheck.reason); + setTimeout(() => setMessage(null), 3000); + } return; } @@ -366,7 +381,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", @@ -382,15 +400,26 @@ 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); - setShowConfetti(true); - setTimeout(() => setShowConfetti(false), 3000); - setFlag(""); - setTimeout(() => setIsDone(true), 5000); - setTimeout(() => router.push("/problems"), 8000); + if (isRight) { + setIsIncorrect(false); + 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( () => (lastSubmittedFlag.current = ""), MIN_SUBMISSION_INTERVAL @@ -398,6 +427,7 @@ const Page = ({ params }: { params: Promise }) => { } } else { setMessage(result.message || "An error occurred"); + setIsIncorrect(true); setTimeout( () => (lastSubmittedFlag.current = ""), MIN_SUBMISSION_INTERVAL @@ -425,6 +455,29 @@ const Page = ({ params }: { params: Promise }) => { } }; + const handleFlagChange = (e: React.ChangeEvent) => { + setFlag(e.target.value); + if (isIncorrect) { + setIsIncorrect(false); + } + }; + + 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(); @@ -438,7 +491,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"; } @@ -453,8 +506,11 @@ 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 ; - + // Allow viewing the problem without authentication // Authentication will be required only when submitting flags @@ -549,24 +605,105 @@ const Page = ({ params }: { params: Promise }) => { />
)} - {isDone ? ( + {isDone && !isPracticeMode ? (
-
-
- 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 + +
+

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

+
+
+
+
+ Challenge completed +

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

+
+
@@ -609,6 +746,11 @@ const Page = ({ params }: { params: Promise }) => { Solved )} + {isPracticeMode && ( + + Practice mode + + )}
@@ -627,6 +769,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 && ( @@ -750,13 +914,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} }) => {

) : (

- 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 @@ -853,36 +1024,40 @@ const Page = ({ params }: { params: Promise }) => {

Submit Flag

setFlag(e.target.value)} + onChange={handleFlagChange} onKeyPress={handleKeyPress} - disabled={submitting || isCorrect || isExpired} + disabled={isSubmissionLocked} maxLength={100} /> {/* Time remaining display */} diff --git a/app/(main)/problems/page.tsx b/app/(main)/problems/page.tsx index 47299d2..1346595 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"; @@ -453,30 +453,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(); @@ -641,7 +648,8 @@ const Page: React.FC = () => { // Note: Removed authentication check to allow public browsing // Authentication will be required only when solving challenges - const shouldShowPagination = problems.length > 0 || currentPage > 1; + const shouldShowPagination = + !isSearchActive && (problems.length > 0 || currentPage > 1); return (
diff --git a/app/(main)/profile/page.tsx b/app/(main)/profile/page.tsx index efacbf0..6f55e5e 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}`} 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/instagram/route.ts b/app/api/instagram/route.ts new file mode 100644 index 0000000..94849c0 --- /dev/null +++ b/app/api/instagram/route.ts @@ -0,0 +1,73 @@ +import { NextResponse } from "next/server"; +import fallbackPosts from "@/lib/instagram-data.json"; + +// Simple in-memory cache to avoid hitting rate limits +let cache: { data: any; timestamp: number } | null = null; +const CACHE_DURATION = 3600 * 1000; // 1 hour + +const buildFallbackPosts = () => + 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; + + // If credentials are not provided, use fallback data + if (!accessToken || !businessId) { + console.warn("Instagram credentials missing. Using fallback data."); + return NextResponse.json(buildFallbackPosts().slice(0, 3)); + } + + // Check cache + if (cache && Date.now() - cache.timestamp < CACHE_DURATION) { + return NextResponse.json(cache.data); + } + + try { + const response = await fetch( + `https://graph.facebook.com/v21.0/${businessId}/media?fields=id,caption,media_type,media_url,permalink,timestamp&access_token=${accessToken}&limit=10` + ); + + if (!response.ok) { + const errorData = await response.json(); + console.error("Instagram Graph API Error:", errorData); + throw new Error(errorData.error?.message || "Failed to fetch from Instagram"); + } + + const { data } = await response.json(); + + // Filter for images and map to our internal format + const formattedPosts = data + .filter((item: any) => item.media_type === "IMAGE" || item.media_type === "CAROUSEL_ALBUM") + .slice(0, 3) + .map((item: any) => ({ + id: item.id, + link: item.permalink, + imgUrl: item.media_url, + caption: item.caption || "", + timestamp: item.timestamp, + })); + + // Update cache + cache = { + data: formattedPosts, + timestamp: Date.now(), + }; + + return NextResponse.json(formattedPosts); + } catch (error) { + console.error("Instagram API Route Error:", error); + // Fallback to static data if API fails + return NextResponse.json(buildFallbackPosts().slice(0, 3)); + } +} diff --git a/app/api/problems/[id]/route.ts b/app/api/problems/[id]/route.ts index c8a47f0..2f0180d 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 @@ -135,6 +140,7 @@ export async function GET( return NextResponse.json({ question: questionData, + hintCount, isDone, expired, timeRemaining, @@ -170,6 +176,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); @@ -194,8 +201,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!" }, @@ -204,9 +227,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 { @@ -260,6 +280,7 @@ export async function POST( message: "Right! Congratulations on solving the challenge!", points: finalPoints, success: true, + correct: true, }, { status: HttpStatusCode.Ok } ); @@ -276,6 +297,7 @@ export async function POST( { message: "Incorrect flag. Try again!", success: false, + correct: false, }, { status: HttpStatusCode.Ok } ); 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

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 && ( {session.status === "authenticated" ? ( @@ -263,14 +276,31 @@ const Navbar: React.FC = () => {
- @@ -319,7 +349,7 @@ const Navbar: React.FC = () => { ); })} - + {/* Show sign in/up button for unauthenticated users */} {session.status === "unauthenticated" && (
  • 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} diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx index 290e935..87c2dc6 100644 --- a/components/ui/dropdown-menu.tsx +++ b/components/ui/dropdown-menu.tsx @@ -27,7 +27,7 @@ const DropdownMenuSubTrigger = React.forwardRef< { - 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'); }, }, }; diff --git a/next.config.mjs b/next.config.mjs index b60c879..1a006cc 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -22,6 +22,22 @@ const nextConfig = { protocol: "https", hostname: "prod-files-secure.s3.us-west-2.amazonaws.com", }, + { + protocol: "https", + hostname: "images.unsplash.com", + }, + { + protocol: "https", + hostname: "*.cdninstagram.com", + }, + { + protocol: "https", + hostname: "*.fbcdn.net", + }, + { + protocol: "https", + hostname: "www.instagram.com", + }, ], formats: ["image/avif", "image/webp"], minimumCacheTTL: 60, @@ -67,7 +83,7 @@ const nextConfig = { "default-src 'self'; " + "script-src 'self' 'unsafe-eval' 'unsafe-inline' https://pagead2.googlesyndication.com https://www.googletagmanager.com https://accounts.google.com https://va.vercel-scripts.com; " + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " + - "img-src 'self' https://lh3.googleusercontent.com https://prod-files-secure.s3.us-west-2.amazonaws.com data: https://pagead2.googlesyndication.com https://www.google.com; " + + "img-src 'self' https://lh3.googleusercontent.com https://prod-files-secure.s3.us-west-2.amazonaws.com data: https://pagead2.googlesyndication.com https://www.google.com https://www.instagram.com https://*.cdninstagram.com https://*.fbcdn.net; " + "font-src 'self' https://fonts.gstatic.com data:; " + "connect-src 'self' https://pagead2.googlesyndication.com https://www.google-analytics.com https://stats.g.doubleclick.net https://*.vercel-analytics.com; " + "frame-src 'self' https://googleads.g.doubleclick.net https://accounts.google.com; " + @@ -90,7 +106,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", diff --git a/utils/data.ts b/utils/data.ts index d59e4d1..142d124 100644 --- a/utils/data.ts +++ b/utils/data.ts @@ -103,4 +103,4 @@ export const initialQuestion: Questions = { flag: "", isSolved: false, done: false -}; \ No newline at end of file +};