diff --git a/eslint.config.js b/eslint.config.js
index aeda54325f..a98edbf6cb 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -300,9 +300,6 @@ module.exports = [
'src/app/(setup)/setup/page.tsx',
'src/app/\\[...recipient\\]/client.tsx',
'src/app/crisp-proxy/page.tsx',
- 'src/app/quests/\\[questId\\]/page.tsx',
- 'src/app/quests/components/QuestsHero.tsx',
- 'src/app/quests/explore/page.tsx',
'src/app/recover-wallet/page.tsx',
'src/components/AddMoney/components/MantecaAddMoney.tsx',
'src/components/AddWithdraw/AddWithdrawCountriesList.tsx',
diff --git a/redirects.json b/redirects.json
index 10b16321ef..54d1dea3d1 100644
--- a/redirects.json
+++ b/redirects.json
@@ -183,7 +183,17 @@
},
{
"source": "/devconnect",
- "destination": "/quests",
+ "destination": "/",
+ "permanent": false
+ },
+ {
+ "source": "/quests",
+ "destination": "/",
+ "permanent": false
+ },
+ {
+ "source": "/quests/:path*",
+ "destination": "/",
"permanent": false
},
{
diff --git a/scripts/native-build.js b/scripts/native-build.js
index 26d9f7d79b..3755a16591 100644
--- a/scripts/native-build.js
+++ b/scripts/native-build.js
@@ -16,7 +16,7 @@ const APP_DIR = path.join(__dirname, '..', 'src', 'app')
// Files/folders to temporarily disable during native build.
// the native app only needs (mobile-ui) and (setup) routes.
-// everything else (marketing, blog, quests, locale pages) is web-only.
+// everything else (marketing, blog, locale pages) is web-only.
const ITEMS_TO_DISABLE = [
{ path: 'api', type: 'dir' },
{ path: 'sitemap.ts', type: 'file' },
@@ -29,9 +29,6 @@ const ITEMS_TO_DISABLE = [
{ path: 'es-419', type: 'dir' }, // localized landing (web-only)
{ path: 'es-ar', type: 'dir' }, // localized landing (web-only)
{ path: 'pt-br', type: 'dir' }, // localized landing (web-only)
- { path: 'quests/[questId]', type: 'dir' }, // quest detail page (dynamicParams issues)
- { path: 'quests/explore', type: 'dir' }, // quest explore page
- { path: 'quests/page.tsx', type: 'file' }, // quest list page
{ path: 'invite', type: 'dir' }, // invite landing pages
{ path: 'exchange', type: 'dir' }, // exchange pages
{ path: 'privacy', type: 'dir' }, // legal pages
diff --git a/src/app/quests/[questId]/page.tsx b/src/app/quests/[questId]/page.tsx
deleted file mode 100644
index 61a55e9537..0000000000
--- a/src/app/quests/[questId]/page.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-'use client'
-
-import { use } from 'react'
-import Layout from '@/components/Global/Layout'
-import { useRouter, useSearchParams } from 'next/navigation'
-import { motion } from 'framer-motion'
-import Image from 'next/image'
-import { Button } from '@/components/0_Bruddle/Button'
-import { QuestLeaderboard } from '../components/QuestLeaderboard'
-import { UserRankCard } from '../components/UserRankCard'
-import { QUEST_CONFIG, getQuestStatus, type QuestId } from '../constants'
-import { QuestHeroDecoration } from '../components/QuestHeroDecoration'
-import { QuestArrowCta } from '../components/QuestArrowCta'
-import { useQuestLeaderboard } from '../hooks/useQuests'
-import Loading from '@/components/Global/Loading'
-import { useAuth } from '@/context/authContext'
-
-interface QuestDetailPageProps {
- params: Promise<{ questId: string }>
-}
-
-export default function QuestDetailPage(props: QuestDetailPageProps) {
- const params = use(props.params)
- const router = useRouter()
- const searchParams = useSearchParams()
- const { userId, username } = useAuth()
- const useTestTimePeriod = searchParams?.get('useTestTimePeriod') === 'true'
- const questStatus = getQuestStatus()
- const questId = params.questId as QuestId
- const { data: questData, isLoading } = useQuestLeaderboard(questId, 10, useTestTimePeriod)
-
- const questConfig = QUEST_CONFIG[questId]
-
- if (!questConfig) {
- return (
-
-
-
-
Quest Not Found
- router.push('/quests')}>Back to Quests
-
-
-
- )
- }
-
- const leaderboard = questData?.leaderboard || []
- const userStatus = questData?.userStatus
- const isAuthenticated = !!userId
-
- const bgColorClass =
- questConfig.backgroundColor === 'purple'
- ? 'bg-purple-200'
- : questConfig.backgroundColor === 'pink'
- ? 'bg-pink-200'
- : 'bg-blue-200'
-
- return (
-
-
-
-
- {/* Content */}
-
- {/* Back button - Same as explore page */}
-
router.push('/quests')}
- className="mb-8 inline-flex items-center gap-2 rounded-sm border-2 border-black bg-white px-4 py-2 font-bold text-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] transition-all duration-100 hover:-translate-y-0.5 hover:shadow-[5px_5px_0px_0px_rgba(0,0,0,1)] active:translate-x-1 active:translate-y-1 active:shadow-none"
- >
- ←
- Back
-
-
- {/* Quest Header Card */}
-
-
-
-
-
-
-
{questConfig.title}
-
{questConfig.explainer}
-
-
-
-
- {/* Leaderboard */}
-
- {isLoading ? (
-
-
-
- ) : leaderboard.length === 0 &&
- !userStatus &&
- questStatus === 'not_started' &&
- !useTestTimePeriod ? (
-
-
⏰
-
Coming Soon!
-
Leaderboard will be available on November 17th, 2025
-
- ) : (
-
- {leaderboard.length === 0 ? (
-
-
🏆
-
No entries yet
-
Be the first to compete!
-
- ) : (
-
- )}
-
- {/* User Status - shown as last entry if authenticated */}
- {isAuthenticated && userStatus && username && (
-
- )}
-
- )}
-
-
- {/* Main CTA - Like Landing Page */}
-
router.push('/setup')}
- className="mt-16 mb-12"
- buttonClassName="bg-white px-7 py-3 text-base font-extrabold hover:bg-white/90 md:px-9 md:py-8 md:text-xl"
- />
-
-
-
- )
-}
diff --git a/src/app/quests/__tests__/footer.test.tsx b/src/app/quests/__tests__/footer.test.tsx
deleted file mode 100644
index 2ccc6e4b3d..0000000000
--- a/src/app/quests/__tests__/footer.test.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Children, isValidElement, type ReactElement, type ReactNode } from 'react'
-import Footer from '@/components/LandingPage/Footer'
-import QuestsPage from '../page'
-
-jest.mock('next/font/google', () => ({
- Roboto_Flex: () => ({ className: 'roboto-flex' }),
-}))
-
-describe('Quests footer composition', () => {
- it('retains the server footer, including its SEO site directory', () => {
- const page = QuestsPage() as ReactElement<{ children: ReactNode }>
- const children = Children.toArray(page.props.children)
-
- expect(children.some((child) => isValidElement(child) && child.type === Footer)).toBe(true)
- })
-})
diff --git a/src/app/quests/components/QuestArrowCta.tsx b/src/app/quests/components/QuestArrowCta.tsx
deleted file mode 100644
index a21991e9bb..0000000000
--- a/src/app/quests/components/QuestArrowCta.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-'use client'
-
-import Image from 'next/image'
-import { motion } from 'framer-motion'
-import { Button } from '@/components/0_Bruddle/Button'
-import { twMerge } from '@/utils/tw'
-
-/**
- * The landing-page-style CTA button with the four hand-drawn arrows, shared by
- * the three quest surfaces. Label / target / spacing differ per caller.
- */
-export function QuestArrowCta({
- label,
- onClick,
- className,
- buttonClassName,
-}: {
- label: string
- onClick: () => void
- className?: string
- buttonClassName?: string
-}) {
- return (
-
-
- {label}
-
- {/* Arrows like landing page */}
-
-
-
-
-
- )
-}
diff --git a/src/app/quests/components/QuestCard.tsx b/src/app/quests/components/QuestCard.tsx
deleted file mode 100644
index 5613caf4be..0000000000
--- a/src/app/quests/components/QuestCard.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-'use client'
-
-import Image from 'next/image'
-import { motion } from 'framer-motion'
-import { useRouter } from 'next/navigation'
-import { formatExtendedNumber } from '@/utils/general.utils'
-import Loading from '@/components/Global/Loading'
-import { PRIZE_TIERS } from '../constants'
-import type { LeaderboardEntry } from '../types'
-import { UserRankCard } from './UserRankCard'
-
-export interface QuestCardProps {
- title: string
- description: string
- iconPath: string
- leaderboard: LeaderboardEntry[]
- badgeColor: string
- backgroundColor: string
- onClick?: () => void
- questStatus?: 'not_started' | 'active' | 'ended'
- isLoading?: boolean
- isCurrency?: boolean
- hasUserData?: boolean
- useTestTimePeriod?: boolean
- userStatus?: { metric: number }
- username?: string
- isAuthenticated?: boolean
-}
-
-export function QuestCard({
- title,
- description,
- iconPath,
- leaderboard,
- badgeColor,
- backgroundColor,
- onClick,
- questStatus = 'active',
- isLoading = false,
- isCurrency = false,
- hasUserData = false,
- useTestTimePeriod = false,
- userStatus,
- username,
- isAuthenticated = false,
-}: QuestCardProps) {
- const router = useRouter()
- const getBadgeColorClasses = (color: string) => {
- switch (color) {
- case 'YELLOW':
- return 'bg-yellow-200 text-foreground-primary'
- case 'PINK':
- return 'bg-pink-200 text-pink-700'
- case 'BLUE':
- return 'bg-blue-200 text-blue-600'
- default:
- return 'bg-gray-100 text-gray-700'
- }
- }
-
- const bgColorClass =
- backgroundColor === 'purple' ? 'bg-purple-200' : backgroundColor === 'pink' ? 'bg-pink-200' : 'bg-blue-200'
-
- return (
-
- {/* Quest Icon and Title - Clickable Header */}
-
-
-
-
-
-
{title}
-
{description}
-
-
-
- {/* Mini Leaderboard - Top 3 or Empty State */}
-
- {isLoading ? (
-
-
-
- ) : leaderboard.length === 0 && !hasUserData && questStatus === 'not_started' && !useTestTimePeriod ? (
-
-
⏰
-
Coming Soon!
-
Starts Nov 17th
-
- ) : leaderboard.length === 0 ? (
-
-
🏆
-
No entries yet
-
Be the first!
-
- ) : (
- <>
- {leaderboard.slice(0, 3).map((entry) => (
-
{
- e.stopPropagation()
- router.push(`/${entry.username}`)
- }}
- className="flex cursor-pointer items-center justify-between border-2 border-black bg-white p-2.5 shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] transition-all duration-100 hover:-translate-y-0.5 hover:shadow-[5px_5px_0px_0px_rgba(0,0,0,1)] active:translate-x-1 active:translate-y-1 active:shadow-none md:p-3"
- >
-
- {/* Rank Badge */}
-
- {entry.rank === 1 && 🥇 }
- {entry.rank === 2 && 🥈 }
- {entry.rank === 3 && 🥉 }
-
-
- {/* Username */}
-
-
-
- {entry.username}
-
- {entry.rank <= 3 && (
-
- {PRIZE_TIERS[entry.rank as keyof typeof PRIZE_TIERS]}
-
- )}
-
-
-
-
- {/* Metric */}
-
- {isCurrency && '$'}
- {formatExtendedNumber(Math.round(entry.metric), 6)}
-
-
- ))}
-
- {/* User Status - shown as last entry if authenticated */}
- {isAuthenticated && userStatus && username && (
-
- )}
- >
- )}
-
-
- )
-}
diff --git a/src/app/quests/components/QuestHeroDecoration.tsx b/src/app/quests/components/QuestHeroDecoration.tsx
deleted file mode 100644
index 58689e1432..0000000000
--- a/src/app/quests/components/QuestHeroDecoration.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-'use client'
-
-import { useCallback, useEffect, useState } from 'react'
-import { motion } from 'framer-motion'
-import borderCloud from '@/assets/illustrations/border-cloud.svg'
-import Star from '@/assets/illustrations/star.svg'
-
-/**
- * The animated cloud + star backdrop shared by the three quest surfaces
- * (QuestsHero, /quests/explore, /quests/[questId]). One copy of the
- * drifting-cloud math and the fixed decoration positions instead of three.
- */
-export function QuestHeroDecoration({ secondStarTop = '60%' }: { secondStarTop?: '50%' | '60%' }) {
- const [screenWidth, setScreenWidth] = useState(0)
-
- useEffect(() => {
- const handleResize = () => setScreenWidth(window.innerWidth)
- handleResize()
- window.addEventListener('resize', handleResize)
- return () => window.removeEventListener('resize', handleResize)
- }, [])
-
- const createCloudAnimation = useCallback(
- (side: 'left' | 'right', width: number, speed: number) => {
- const vpWidth = screenWidth || 1080
- const totalDistance = vpWidth + width
-
- return {
- initial: { x: side === 'left' ? -width : vpWidth },
- animate: { x: side === 'left' ? vpWidth : -width },
- transition: {
- ease: 'linear' as const,
- duration: totalDistance / speed,
- repeat: Infinity,
- },
- }
- },
- [screenWidth]
- )
-
- return (
- <>
- {/* Animated Clouds - Reduced for performance */}
-
-
-
-
-
-
- {/* Animated Stars - Fade in like landing page */}
-
-
- >
- )
-}
diff --git a/src/app/quests/components/QuestLeaderboard.tsx b/src/app/quests/components/QuestLeaderboard.tsx
deleted file mode 100644
index e4fba395ea..0000000000
--- a/src/app/quests/components/QuestLeaderboard.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-'use client'
-
-import { motion } from 'framer-motion'
-import { useRouter } from 'next/navigation'
-import { formatExtendedNumber } from '@/utils/general.utils'
-import { PRIZE_TIERS } from '../constants'
-import type { LeaderboardEntry } from '../types'
-
-interface QuestLeaderboardProps {
- entries: LeaderboardEntry[]
- metricLabel: string
- badgeColor: string
- isCurrency?: boolean
-}
-
-export function QuestLeaderboard({ entries, badgeColor, isCurrency = false }: QuestLeaderboardProps) {
- const router = useRouter()
- const getBadgeColorClasses = (color: string) => {
- switch (color) {
- case 'YELLOW':
- return 'bg-yellow-200 text-foreground-primary'
- case 'PINK':
- return 'bg-pink-200 text-pink-700'
- case 'BLUE':
- return 'bg-blue-200 text-blue-600'
- default:
- return 'bg-gray-100 text-gray-700'
- }
- }
-
- return (
-
- {entries.map((entry, index) => (
-
router.push(`/${entry.username}`)}
- className="flex cursor-pointer items-center justify-between border-2 border-black bg-white p-2.5 shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] transition-all duration-100 hover:-translate-y-0.5 hover:shadow-[5px_5px_0px_0px_rgba(0,0,0,1)] active:translate-x-1 active:translate-y-1 active:shadow-none md:p-3"
- >
-
- {/* Rank Badge */}
-
- {entry.rank === 1 && 🥇 }
- {entry.rank === 2 && 🥈 }
- {entry.rank === 3 && 🥉 }
- {entry.rank > 3 && #{entry.rank} }
-
-
- {/* Username */}
-
-
- {entry.username}
- {entry.rank <= 3 && (
-
- {PRIZE_TIERS[entry.rank as keyof typeof PRIZE_TIERS]}
-
- )}
-
-
-
-
- {/* Metric */}
-
- {isCurrency && '$'}
- {formatExtendedNumber(Math.round(entry.metric), 6)}
-
-
- ))}
-
- )
-}
diff --git a/src/app/quests/components/QuestsHero.tsx b/src/app/quests/components/QuestsHero.tsx
deleted file mode 100644
index 82e11a6744..0000000000
--- a/src/app/quests/components/QuestsHero.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-'use client'
-
-import { useMemo } from 'react'
-import { motion } from 'framer-motion'
-import { QuestCard } from './QuestCard'
-import handPointing from '@/assets/illustrations/got-it-hand.svg'
-import { useRouter, useSearchParams } from 'next/navigation'
-import Image from 'next/image'
-import { QUEST_CONFIG, getQuestStatus } from '../constants'
-import { QuestHeroDecoration } from './QuestHeroDecoration'
-import { QuestArrowCta } from './QuestArrowCta'
-import { useAllQuestsLeaderboards } from '../hooks/useQuests'
-import { useAuth } from '@/context/authContext'
-
-interface QuestData {
- id: string
- title: string
- description: string
- iconPath: string
- badgeColor: string
- backgroundColor: string
- leaderboard: Array<{
- rank: number
- userId: string
- username: string
- metric: number
- badge?: string
- }>
- hasUserData: boolean
-}
-
-export function QuestsHero() {
- const router = useRouter()
- const searchParams = useSearchParams()
- const { userId, username } = useAuth()
- const useTestTimePeriod = searchParams?.get('useTestTimePeriod') === 'true'
- const questStatus = getQuestStatus()
- const { data: questsData, isLoading } = useAllQuestsLeaderboards(3, useTestTimePeriod)
- const isAuthenticated = !!userId
-
- const quests: QuestData[] = useMemo(() => {
- if (!questsData) {
- return [
- { ...QUEST_CONFIG.most_invites, leaderboard: [], hasUserData: false },
- { ...QUEST_CONFIG.bank_drainer, leaderboard: [], hasUserData: false },
- { ...QUEST_CONFIG.biggest_pot, leaderboard: [], hasUserData: false },
- ]
- }
-
- return [
- {
- ...QUEST_CONFIG.most_invites,
- leaderboard: questsData.most_invites?.leaderboard || [],
- hasUserData: !!questsData.most_invites?.userStatus,
- },
- {
- ...QUEST_CONFIG.bank_drainer,
- leaderboard: questsData.bank_drainer?.leaderboard || [],
- hasUserData: !!questsData.bank_drainer?.userStatus,
- },
- {
- ...QUEST_CONFIG.biggest_pot,
- leaderboard: questsData.biggest_pot?.leaderboard || [],
- hasUserData: !!questsData.biggest_pot?.userStatus,
- },
- ]
- }, [questsData])
-
- return (
-
-
-
- {/* Content */}
-
- {/* Header */}
-
-
-
JOIN PEANUT AT
-
-
-
-
-
- TAKE PART IN OUR{' '}
-
- QUESTS
-
- , COMPETE, AND WIN UP TO $1500 !
-
-
-
- {/* Reuse landing page button style with arrows */}
-
router.push('/quests/explore')}
- className="mt-6 mb-8 md:mt-12 md:mb-12"
- buttonClassName="bg-white px-6 py-2.5 text-sm font-extrabold hover:bg-white/90 md:px-9 md:py-8 md:text-xl"
- />
-
-
- {/* Quests Leaderboards Section */}
-
-
-
-
QUESTS LEADERBOARDS
-
-
-
-
- {/* Quest Cards Grid with Top 3 */}
-
- {quests.map((quest, index) => (
-
- router.push(`/quests/${quest.id}`)}
- questStatus={questStatus}
- isLoading={isLoading}
- isCurrency={quest.id === 'bank_drainer'}
- hasUserData={quest.hasUserData}
- useTestTimePeriod={useTestTimePeriod}
- userStatus={
- quest.id === 'most_invites'
- ? questsData?.most_invites?.userStatus
- : quest.id === 'bank_drainer'
- ? questsData?.bank_drainer?.userStatus
- : questsData?.biggest_pot?.userStatus
- }
- username={username}
- isAuthenticated={isAuthenticated}
- />
-
- ))}
-
-
-
- )
-}
diff --git a/src/app/quests/components/UserRankCard.tsx b/src/app/quests/components/UserRankCard.tsx
deleted file mode 100644
index dfdfdbf754..0000000000
--- a/src/app/quests/components/UserRankCard.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-'use client'
-
-import { motion } from 'framer-motion'
-import { formatExtendedNumber } from '@/utils/general.utils'
-
-interface UserRankCardProps {
- metric: number
- username: string
- isCurrency?: boolean
- backgroundColor?: string
-}
-
-export function UserRankCard({ metric, username, isCurrency = false, backgroundColor = 'white' }: UserRankCardProps) {
- const bgColorClass =
- backgroundColor === 'purple'
- ? 'bg-purple-200'
- : backgroundColor === 'pink'
- ? 'bg-pink-200'
- : backgroundColor === 'blue'
- ? 'bg-blue-200'
- : 'bg-white'
-
- return (
-
-
- {/* Rank Badge - Question Mark for User */}
-
- ?
-
-
- {/* Username */}
-
-
- {username}
-
- YOU
-
-
-
-
-
- {/* Metric */}
-
- {isCurrency && '$'}
- {formatExtendedNumber(Math.round(metric), 6)}
-
-
- )
-}
diff --git a/src/app/quests/constants.ts b/src/app/quests/constants.ts
deleted file mode 100644
index a6b6a44eb0..0000000000
--- a/src/app/quests/constants.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * Quest constants shared across all quest pages
- */
-
-// DevConnect 2025 event period - ACTIVATED
-// Started at 11pm Nov 16 UTC (4 hours early)
-export const EVENT_START_UTC = new Date('2025-11-16T23:00:00Z') // Nov 16, 11pm UTC
-export const EVENT_END_UTC = new Date('2025-11-23T02:59:59Z')
-
-// Prize amounts for top 3 positions
-export const PRIZE_TIERS = {
- 1: '$500',
- 2: '$200',
- 3: '$100',
-} as const
-
-export function getQuestStatus(): 'not_started' | 'active' | 'ended' {
- const now = new Date()
- if (now < EVENT_START_UTC) return 'not_started'
- if (now > EVENT_END_UTC) return 'ended'
- return 'active'
-}
-
-export const QUEST_CONFIG = {
- most_invites: {
- id: 'most_invites' as const,
- title: 'Most Invites',
- description: 'Invite the most people to Peanut during DevConnect!',
- explainer:
- 'Invite friends to Peanut during Nov 17-22 and climb the leaderboard! Each successful invitation counts towards your score, and you will get a part of their points, forever.',
- iconPath: '/badges/most_invites.svg',
- badgeColor: 'YELLOW',
- backgroundColor: 'purple',
- metricLabel: 'invites',
- },
- bank_drainer: {
- id: 'bank_drainer' as const,
- title: 'Bank Drainer',
- description: 'Most funds deposited from banks into Peanut!',
- explainer:
- 'Use Banks to deposit money into Peanut during Nov 17-22! US, EU, MX, BR, and ARS countries count. The more you deposit, the higher you rank.',
- iconPath: '/badges/bank_drainer.svg',
- badgeColor: 'PINK',
- backgroundColor: 'pink',
- metricLabel: 'USD deposited',
- },
- biggest_pot: {
- id: 'biggest_pot' as const,
- title: 'Biggest Pot',
- description: 'Get the most unique contributors to a request pot!',
- explainer:
- 'Create the request pot with most unique contributors during Nov 17-22! Each unique user who contributes to your pot counts. Rally your community to win.',
- iconPath: '/badges/biggest_request_pot.svg',
- badgeColor: 'BLUE',
- backgroundColor: 'blue',
- metricLabel: 'unique contributors',
- },
-} as const
-
-export type QuestId = keyof typeof QUEST_CONFIG
diff --git a/src/app/quests/explore/loading.tsx b/src/app/quests/explore/loading.tsx
deleted file mode 100644
index 88a69d6685..0000000000
--- a/src/app/quests/explore/loading.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-export default function Loading() {
- return (
-
- )
-}
diff --git a/src/app/quests/explore/page.tsx b/src/app/quests/explore/page.tsx
deleted file mode 100644
index 614027288d..0000000000
--- a/src/app/quests/explore/page.tsx
+++ /dev/null
@@ -1,203 +0,0 @@
-'use client'
-
-import Layout from '@/components/Global/Layout'
-import { useRouter, useSearchParams } from 'next/navigation'
-import { motion } from 'framer-motion'
-import Image from 'next/image'
-import { useMemo } from 'react'
-import { QuestLeaderboard } from '../components/QuestLeaderboard'
-import { UserRankCard } from '../components/UserRankCard'
-import { QUEST_CONFIG, getQuestStatus } from '../constants'
-import { QuestHeroDecoration } from '../components/QuestHeroDecoration'
-import { QuestArrowCta } from '../components/QuestArrowCta'
-import { useAllQuestsLeaderboards } from '../hooks/useQuests'
-import Loading from '@/components/Global/Loading'
-import { useAuth } from '@/context/authContext'
-
-export default function QuestsExplorePage() {
- const router = useRouter()
- const searchParams = useSearchParams()
- const { userId, username } = useAuth()
- const useTestTimePeriod = searchParams?.get('useTestTimePeriod') === 'true'
- const questStatus = getQuestStatus()
- const { data: questsData, isLoading } = useAllQuestsLeaderboards(10, useTestTimePeriod)
- const isAuthenticated = !!userId
-
- const quests = useMemo(() => {
- if (!questsData) {
- return [
- { ...QUEST_CONFIG.most_invites, leaderboard: [], hasUserData: false },
- { ...QUEST_CONFIG.bank_drainer, leaderboard: [], hasUserData: false },
- { ...QUEST_CONFIG.biggest_pot, leaderboard: [], hasUserData: false },
- ]
- }
-
- return [
- {
- ...QUEST_CONFIG.most_invites,
- leaderboard: questsData.most_invites?.leaderboard || [],
- hasUserData: !!questsData.most_invites?.userStatus,
- },
- {
- ...QUEST_CONFIG.bank_drainer,
- leaderboard: questsData.bank_drainer?.leaderboard || [],
- hasUserData: !!questsData.bank_drainer?.userStatus,
- },
- {
- ...QUEST_CONFIG.biggest_pot,
- leaderboard: questsData.biggest_pot?.leaderboard || [],
- hasUserData: !!questsData.biggest_pot?.userStatus,
- },
- ]
- }, [questsData])
-
- return (
-
-
-
-
- {/* Content */}
-
- {/* Back button */}
-
router.push('/quests')}
- className="mb-8 inline-flex items-center gap-2 rounded-sm border-2 border-black bg-white px-4 py-2 font-bold text-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] transition-all duration-100 hover:-translate-y-0.5 hover:shadow-[5px_5px_0px_0px_rgba(0,0,0,1)] active:translate-x-1 active:translate-y-1 active:shadow-none"
- >
- ←
- Back
-
-
- {/* Header with Card */}
-
-
-
-
-
-
QUESTS
-
- Nov 17-22, 2025 • Compete & Win up to $1500!
-
-
-
-
- {/* Quests Sections - Grid on Desktop */}
-
- {quests.map((quest, index) => {
- const bgColorClass =
- quest.backgroundColor === 'purple'
- ? 'bg-purple-200'
- : quest.backgroundColor === 'pink'
- ? 'bg-pink-200'
- : 'bg-blue-200'
-
- return (
-
router.push(`/quests/${quest.id}`)}
- initial={{ opacity: 0, y: 50 }}
- animate={{ opacity: 1, y: 0 }}
- transition={{
- duration: 0.5,
- delay: index * 0.15,
- ease: [0.25, 0.1, 0.25, 1],
- }}
- >
- {/* Quest Header */}
-
-
-
-
-
-
- {quest.title}
-
-
{quest.explainer}
-
-
-
- {/* Top 10 Leaderboard */}
-
- {isLoading ? (
-
-
-
- ) : quest.leaderboard.length === 0 &&
- !quest.hasUserData &&
- questStatus === 'not_started' &&
- !useTestTimePeriod ? (
-
-
⏰
-
Coming Soon!
-
- Leaderboards will be available on November 17th, 2025
-
-
- ) : quest.leaderboard.length === 0 ? (
-
-
🏆
-
No entries yet
-
Be the first to compete!
-
- ) : (
-
-
-
- {/* User Status - shown as last entry if authenticated */}
- {isAuthenticated &&
- username &&
- (quest.id === 'most_invites'
- ? questsData?.most_invites?.userStatus
- : quest.id === 'bank_drainer'
- ? questsData?.bank_drainer?.userStatus
- : questsData?.biggest_pot?.userStatus) && (
-
- )}
-
- )}
-
-
- )
- })}
-
-
- {/* Main CTA - Like Landing Page */}
-
router.push('/setup')}
- className="mt-16 mb-12"
- buttonClassName="bg-white px-7 py-3 text-base font-extrabold hover:bg-white/90 md:px-9 md:py-8 md:text-xl"
- />
-
-
-
- )
-}
diff --git a/src/app/quests/hooks/useQuests.ts b/src/app/quests/hooks/useQuests.ts
deleted file mode 100644
index be0633ca80..0000000000
--- a/src/app/quests/hooks/useQuests.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * Quest data fetching hooks using TanStack Query
- * Provides caching and automatic refetching for quest leaderboards
- */
-
-import { useQuery } from '@tanstack/react-query'
-import { questsApi } from '@/services/quests'
-import { getQuestStatus } from '../constants'
-
-export function useAllQuestsLeaderboards(limit: number = 3, useTestTimePeriod: boolean = false) {
- const questStatus = getQuestStatus()
- const isEnabled = useTestTimePeriod || questStatus !== 'not_started'
-
- return useQuery({
- queryKey: ['quests', 'all-leaderboards', limit, useTestTimePeriod],
- queryFn: async () => {
- const result = await questsApi.getAllLeaderboards({ limit, useTestTimePeriod })
- if (!result.success) {
- throw new Error(result.error || 'Failed to fetch leaderboards')
- }
- return result.data
- },
- enabled: isEnabled,
- staleTime: 5 * 60 * 1000, // 5 minutes (matches backend cache)
- refetchInterval: 5 * 60 * 1000,
- })
-}
-
-export function useQuestLeaderboard(
- questId: 'most_invites' | 'bank_drainer' | 'biggest_pot',
- limit: number = 10,
- useTestTimePeriod: boolean = false
-) {
- const questStatus = getQuestStatus()
- const isEnabled = useTestTimePeriod || questStatus !== 'not_started'
-
- return useQuery({
- queryKey: ['quests', questId, 'leaderboard', limit, useTestTimePeriod],
- queryFn: async () => {
- const result = await questsApi.getQuestLeaderboard(questId, { limit, useTestTimePeriod })
- if (!result.success) {
- throw new Error(result.error || 'Failed to fetch leaderboard')
- }
- return result.data
- },
- enabled: isEnabled,
- staleTime: 5 * 60 * 1000, // 5 minutes (matches backend cache)
- refetchInterval: 5 * 60 * 1000,
- })
-}
diff --git a/src/app/quests/layout.tsx b/src/app/quests/layout.tsx
deleted file mode 100644
index 17ec5fd9ad..0000000000
--- a/src/app/quests/layout.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { type Metadata } from 'next'
-import { generateMetadata as metadataHelper } from '@/app/metadata'
-
-// Keep the app route self-canonical and noindex. It is also robots-disallowed;
-// metadata backs that up for URL-only hits.
-export const metadata: Metadata = {
- ...metadataHelper({
- title: 'Quests | Peanut',
- description: 'Complete quests, earn rewards, and get the most out of Peanut.',
- canonical: '/quests',
- }),
- robots: { index: false, follow: false },
-}
-
-export default function QuestsLayout({ children }: { children: React.ReactNode }) {
- return children
-}
diff --git a/src/app/quests/page.tsx b/src/app/quests/page.tsx
deleted file mode 100644
index fdb432114e..0000000000
--- a/src/app/quests/page.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import Layout from '@/components/Global/Layout'
-import { DropLink, FAQs, Marquee, NoFees, SendInSeconds } from '@/components/LandingPage'
-import { SecurityBuiltIn } from '@/components/LandingPage/securityBuiltIn'
-import { YourMoney } from '@/components/LandingPage/yourMoney'
-import { RegulatedRails } from '@/components/LandingPage/RegulatedRails'
-import Footer from '@/components/LandingPage/Footer'
-import Manteca from '@/components/LandingPage/Manteca'
-import { QuestsHero } from './components/QuestsHero'
-import { EN_LANDING_STRINGS } from '@/components/LandingPage/landingStrings'
-import { EN_LANDING_CONTENT_HREFS } from '@/components/LandingPage/landingContentHrefs'
-
-export default function QuestsPage() {
- const marqueeProps = {
- visible: true,
- message: ['QUESTS', 'LEADERBOARDS', 'WIN $1500', 'DEVCONNECT', 'COMPETE', 'EARN BADGES'],
- }
-
- const faqs = {
- heading: 'Faqs',
- questions: [
- {
- id: '0',
- question: 'What are Peanut Quests?',
- answer: `Peanut Quests are fun challenges where you can compete with other users to climb the leaderboard and win prizes!`,
- },
- {
- id: '1',
- question: 'How do I participate?',
- answer: 'Simply sign up for Peanut and start using the app! Your activities automatically count towards various quests.',
- },
- {
- id: '2',
- question: 'What can I win?',
- answer: 'The top scorer during DevConnect can win up to $1500! Plus, earn exclusive badges to show off your achievements.',
- },
- {
- id: '3',
- question: 'When does the competition end?',
- answer: 'The DevConnect quest competition runs throughout the event. Check the leaderboards regularly to see your ranking!',
- },
- ],
- marquee: {
- visible: false,
- message: 'Peanut',
- },
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/src/app/quests/types.ts b/src/app/quests/types.ts
deleted file mode 100644
index d507859102..0000000000
--- a/src/app/quests/types.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Shared TypeScript interfaces for Quest components
- */
-
-export interface LeaderboardEntry {
- rank: number
- userId: string
- username: string
- metric: number
- medal?: string
-}
-
-export interface QuestLeaderboardData {
- leaderboard: LeaderboardEntry[]
- userStatus?: {
- metric: number
- }
-}
-
-export interface AllQuestsLeaderboardData {
- most_invites: QuestLeaderboardData
- bank_drainer: QuestLeaderboardData
- biggest_pot: QuestLeaderboardData
-}
diff --git a/src/components/LandingPage/Manteca.tsx b/src/components/LandingPage/Manteca.tsx
index 911a0302c0..360d3d5ed5 100644
--- a/src/components/LandingPage/Manteca.tsx
+++ b/src/components/LandingPage/Manteca.tsx
@@ -19,7 +19,7 @@ const starConfigs = [
{ className: 'absolute bottom-20 right-44', delay: '0.6s', rotate: '22deg' },
]
-// Cream, as on /quests. The homepage passes blue instead — there it follows
+// Cream default; the homepage passes blue instead — there it follows
// RegulatedRails, which is already cream.
const DEFAULT_BG_COLOR = '#F9F4F0'
diff --git a/src/components/LandingPage/PartnerLockup.tsx b/src/components/LandingPage/PartnerLockup.tsx
index fe9e455a17..3fbdd1bfea 100644
--- a/src/components/LandingPage/PartnerLockup.tsx
+++ b/src/components/LandingPage/PartnerLockup.tsx
@@ -7,7 +7,7 @@ import { twMerge } from '@/utils/tw'
* the rail.
*
* The mark sits directly on the section ground — black is 9:1 on the homepage
- * blue and 19:1 on the /quests cream, so it needs no card (unlike
+ * blue, so it needs no card (unlike
* `#regulated-rails`, whose white inner card exists because those marks sit on
* pink). The fixed-height slot keeps the two marks on a shared baseline; the
* underlined "Learn more →" is the only click affordance, so it stays.
diff --git a/src/components/LandingPage/landingContentHrefs.ts b/src/components/LandingPage/landingContentHrefs.ts
index 56332b125c..98b726cb79 100644
--- a/src/components/LandingPage/landingContentHrefs.ts
+++ b/src/components/LandingPage/landingContentHrefs.ts
@@ -1,8 +1,7 @@
/**
* Canonical English homepage content links. Server components resolve these to
* their locale owner via contentHrefsFor (landingContentHrefs.server.ts);
- * client-reachable consumers receive the resolved map as a prop, or this map
- * verbatim on English-only pages (currently /quests).
+ * client-reachable consumers receive the resolved map as a prop.
*/
export const EN_LANDING_CONTENT_HREFS = {
pricing: '/en/pricing',
diff --git a/src/components/LandingPage/landingStrings.ts b/src/components/LandingPage/landingStrings.ts
index f73d890fc3..baebb0ad7f 100644
--- a/src/components/LandingPage/landingStrings.ts
+++ b/src/components/LandingPage/landingStrings.ts
@@ -1,4 +1,3 @@
-import en from '@/i18n/en.json'
import { type Translations } from '@/i18n/types'
import type { LandingProblemStrings, LandingSupportedRailsStrings } from './landing.types'
@@ -97,7 +96,3 @@ export function landingStrings(i18n: Translations): LandingStrings {
},
}
}
-
-// For the English-only pages that reuse landing sections (/quests).
-// Imports just en.json so those client bundles don't pull every locale catalog.
-export const EN_LANDING_STRINGS = landingStrings(en as Translations)
diff --git a/src/components/Marketing/LocaleSwitcher.tsx b/src/components/Marketing/LocaleSwitcher.tsx
index 52c6c6121d..6317ed6027 100644
--- a/src/components/Marketing/LocaleSwitcher.tsx
+++ b/src/components/Marketing/LocaleSwitcher.tsx
@@ -13,8 +13,8 @@ import { DEFAULT_LOCALE, SUPPORTED_LOCALES, type Locale } from '@/i18n/types'
* Same-page href for `target`.
*
* Marketing routes are all `/{locale}/rest`, so switching swaps the first
- * segment. Paths whose first segment is NOT a locale (`/`, `/lp`, `/careers`,
- * `/quests`) have no localized twin, so they resolve to that locale's landing
+ * segment. Paths whose first segment is NOT a locale (`/`, `/lp`, `/careers`)
+ * have no localized twin, so they resolve to that locale's landing
* instead of inventing a `/es-419/lp` that would 404. English landing is `/`,
* not `/en` — `/en` redirects.
*/
diff --git a/src/components/Marketing/__tests__/locale-switcher.test.ts b/src/components/Marketing/__tests__/locale-switcher.test.ts
index cd6dd9ded2..fb166cfe50 100644
--- a/src/components/Marketing/__tests__/locale-switcher.test.ts
+++ b/src/components/Marketing/__tests__/locale-switcher.test.ts
@@ -25,8 +25,8 @@ describe('localeHref', () => {
})
it('falls back to the landing for paths with no localized twin', () => {
- // /lp, /exchange, /quests are English-only — never invent /es-419/lp.
- for (const path of ['/lp', '/exchange', '/quests', '/careers']) {
+ // /lp, /exchange are English-only — never invent /es-419/lp.
+ for (const path of ['/lp', '/exchange', '/careers']) {
expect(localeHref(path, 'es-419')).toBe('/es-419')
expect(localeHref(path, 'en')).toBe('/')
}
diff --git a/src/constants/routes.ts b/src/constants/routes.ts
index b74fcbdda7..23ede38cdd 100644
--- a/src/constants/routes.ts
+++ b/src/constants/routes.ts
@@ -30,7 +30,6 @@ export const DEDICATED_ROUTES = [
'profile',
'kyc',
'maintenance',
- 'quests',
'receipt',
'crisp-proxy',
'card',
@@ -110,6 +109,10 @@ export const STATIC_REDIRECT_ROUTES = [
'pints',
'events',
'foodie',
+ // Retired route: /quests and /quests/* redirect to / in redirects.json.
+ // Kept reserved so the catch-all and native deep-link mapper never read
+ // 'quests' as a recipient username.
+ 'quests',
] as const
/**
diff --git a/src/constants/seo-route-policy.js b/src/constants/seo-route-policy.js
index 29a05380cf..a1a49dbda9 100644
--- a/src/constants/seo-route-policy.js
+++ b/src/constants/seo-route-policy.js
@@ -26,7 +26,6 @@ const NOINDEX_ROUTE_PREFIXES = Object.freeze([
'/invite',
'/kyc',
'/maintenance',
- '/quests',
'/receipt',
'/crisp-proxy',
'/card',
@@ -69,7 +68,6 @@ const ROBOTS_DISALLOWED_PATHS = Object.freeze([
'/invite',
'/kyc',
'/maintenance',
- '/quests',
'/receipt',
'/crisp-proxy',
'/card-payment',
diff --git a/src/services/quests.ts b/src/services/quests.ts
deleted file mode 100644
index b3ddad8713..0000000000
--- a/src/services/quests.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-/**
- * Quests API Service
- * Handles all quest-related API calls
- */
-
-import type { QuestLeaderboardData, AllQuestsLeaderboardData } from '@/app/quests/types'
-import { serverFetch } from '@/utils/api-fetch'
-
-export const questsApi = {
- /**
- * Get leaderboards for all quests (top 3 each for landing page)
- * If authenticated, also returns user's status for each quest
- */
- async getAllLeaderboards(params?: { limit?: number; useTestTimePeriod?: boolean }): Promise<{
- success: boolean
- data?: AllQuestsLeaderboardData
- error?: string
- }> {
- try {
- const limit = params?.limit || 3
- const useTestTimePeriod = params?.useTestTimePeriod ? 'true' : 'false'
-
- const response = await serverFetch(
- `/quests/leaderboards?limit=${limit}&useTestTimePeriod=${useTestTimePeriod}`,
- {
- method: 'GET',
- }
- )
-
- if (!response.ok) {
- throw new Error(`Failed to fetch leaderboards: ${response.statusText}`)
- }
-
- const data = await response.json()
- return { success: true, data }
- } catch (error) {
- console.error('Error fetching all quest leaderboards:', error)
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Failed to fetch leaderboards',
- }
- }
- },
-
- /**
- * Get leaderboard for a specific quest (top 10 for detail page)
- * If authenticated, also returns user's status (metric only, not position)
- */
- async getQuestLeaderboard(
- questId: 'most_invites' | 'bank_drainer' | 'biggest_pot',
- params?: { limit?: number; useTestTimePeriod?: boolean }
- ): Promise<{
- success: boolean
- data?: QuestLeaderboardData
- error?: string
- }> {
- try {
- const limit = params?.limit || 10
- const useTestTimePeriod = params?.useTestTimePeriod ? 'true' : 'false'
-
- const response = await serverFetch(
- `/quests/${questId}/leaderboard?limit=${limit}&useTestTimePeriod=${useTestTimePeriod}`,
- {
- method: 'GET',
- }
- )
-
- if (!response.ok) {
- throw new Error(`Failed to fetch ${questId} leaderboard: ${response.statusText}`)
- }
-
- const data = await response.json()
- return { success: true, data }
- } catch (error) {
- console.error(`Error fetching ${questId} leaderboard:`, error)
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Failed to fetch leaderboard',
- }
- }
- },
-}
diff --git a/src/utils/__tests__/friendly-error.utils.test.tsx b/src/utils/__tests__/friendly-error.utils.test.tsx
index 9170598bcf..d9ae66bf19 100644
--- a/src/utils/__tests__/friendly-error.utils.test.tsx
+++ b/src/utils/__tests__/friendly-error.utils.test.tsx
@@ -387,7 +387,6 @@ describe('browser-native fetch rejection (TASK-21956)', () => {
test.each([
['chargesApi.get on a 500', new Error('Failed to fetch charge: Internal Server Error')],
['useLimits on a 503', new Error('Failed to fetch limits: Service Unavailable')],
- ['quests leaderboard', new Error('Failed to fetch leaderboards')],
])('%s is a server error, not lost connectivity', (_case, error) => {
expect(friendlyError(error)).toEqual({ kind: 'code', code: 'genericSupport' })
})
diff --git a/src/utils/__tests__/native-routes.test.ts b/src/utils/__tests__/native-routes.test.ts
index e4e1a65f15..0583eed0bf 100644
--- a/src/utils/__tests__/native-routes.test.ts
+++ b/src/utils/__tests__/native-routes.test.ts
@@ -558,6 +558,11 @@ describe('native-routes', () => {
it('returns null for web-only roots so the caller opens them in the in-app browser', () => {
expect(deepLinkToNativePath('https://peanut.me/help')).toBeNull()
+ // retired /quests: reserved in STATIC_REDIRECT_ROUTES, so it must
+ // never be read as a recipient — the in-app browser opens the web
+ // URL, whose redirects.json entry lands on the homepage.
+ expect(deepLinkToNativePath('https://peanut.me/quests')).toBeNull()
+ expect(deepLinkToNativePath('https://peanut.me/quests/most_invites')).toBeNull()
expect(deepLinkToNativePath('https://peanut.me/blog/some-post')).toBeNull()
expect(deepLinkToNativePath('https://peanut.me/terms')).toBeNull()
expect(deepLinkToNativePath('https://peanut.me/es-419/pricing')).toBeNull()
diff --git a/src/utils/demo-api.ts b/src/utils/demo-api.ts
index f996bf779e..be3e68c096 100644
--- a/src/utils/demo-api.ts
+++ b/src/utils/demo-api.ts
@@ -631,7 +631,7 @@ const ROUTES: Array<{ method: string; pattern: string; handler: Handler }> = [
handler: () => ({ success: true, perk: { sponsored: false, amountSponsored: 0, discountPercentage: 0 } }),
},
- // invites / quests / badges
+ // invites / badges
{ method: 'POST', pattern: '/invites/validate', handler: () => ({ success: true, username: 'demo' }) },
{ method: 'POST', pattern: '/invites/accept', handler: () => ({ success: true }) },
{ method: 'GET', pattern: '/invites/waitlist-position', handler: () => ({ position: null }) },
diff --git a/src/utils/support-context.ts b/src/utils/support-context.ts
index 7427173e6b..afca4f623e 100644
--- a/src/utils/support-context.ts
+++ b/src/utils/support-context.ts
@@ -239,7 +239,6 @@ const IDENTIFIER_PREFIXES = new Set([
'receipt',
'qr',
'qr-pay',
- 'quests',
'profile',
'claim',
'invite',