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

- -
-
-
- ) - } - - 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.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 ( - - - {/* Arrows like landing page */} - Arrow - Arrow - Arrow - Arrow - - ) -} 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} -
-
-

{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 */} - -