diff --git a/__tests__/unit/supabase.pure.test.ts b/__tests__/unit/supabase.pure.test.ts index 8056226..bff598c 100644 --- a/__tests__/unit/supabase.pure.test.ts +++ b/__tests__/unit/supabase.pure.test.ts @@ -11,7 +11,7 @@ vi.mock('@supabase/supabase-js', () => ({ })), })) -import { getHintsFromPuzzle, calculatePercentileBeat } from '@/lib/supabase' +import { getHintsFromPuzzle, calculatePercentileBeat, calculatePuzzlePercentile } from '@/lib/supabase' import type { Puzzle } from '@/lib/supabase' function makePuzzle(overrides: Partial = {}): Puzzle { @@ -164,3 +164,47 @@ describe('calculatePercentileBeat', () => { expect(result).toBe(33) }) }) + +describe('calculatePuzzlePercentile', () => { + it('includes losers when totalAttempts is provided', () => { + // 100 total attempts, 60 wins, 40 losses + const dist = { 1: 15, 2: 20, 3: 15, 4: 10 } + const result = calculatePuzzlePercentile(1, dist, 100) + + // worseThanUser = 40 (losers) + 20+15+10 (worse winners) = 85 + // 85/100 = 85% + expect(result).toBe(85) + }) + + it('falls back to winners-only when totalAttempts not provided', () => { + const dist = { 1: 15, 2: 20, 3: 15, 4: 10 } + const result = calculatePuzzlePercentile(1, dist) + + // worseThanUser = 20+15+10 = 45, totalWins = 60 + // 45/60 = 75% + expect(result).toBe(75) + }) + + it('returns null for empty distribution', () => { + expect(calculatePuzzlePercentile(1, {})).toBeNull() + expect(calculatePuzzlePercentile(1, {}, 0)).toBeNull() + }) + + it('returns 0 for worst guess with no losers', () => { + const dist = { 1: 10, 2: 20, 3: 30, 4: 25, 5: 15 } + const result = calculatePuzzlePercentile(5, dist, 100) + + // No one took more guesses, losers = 0 + expect(result).toBe(0) + }) + + it('counts all losers as worse even for worst guess count', () => { + const dist = { 1: 10, 2: 20, 3: 15, 4: 10, 5: 5 } + // 60 wins, 40 losses + const result = calculatePuzzlePercentile(5, dist, 100) + + // worseThanUser = 40 (losers) + 0 (no worse winners) = 40 + // 40/100 = 40% + expect(result).toBe(40) + }) +}) diff --git a/app/about/page.tsx b/app/about/page.tsx index 7d889ef..7805180 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -128,7 +128,7 @@ export default function AboutPage() {

+
{/* Gradient Background */}
{/* Radial Vignette */} @@ -18,7 +18,7 @@ export default function ArchivePage() {
{/* Content */} -
+
{/* Header */}
{/* Back Button */} @@ -29,8 +29,8 @@ export default function ArchivePage() { - {/* Logo and Title - Centered */} -
+ {/* Logo and Title - Centered, clickable to return home */} +
Radiordle -
+ {/* Spacer for centering */}
diff --git a/components/FeedbackModal.tsx b/components/FeedbackModal.tsx index a11a244..e0bb7aa 100644 --- a/components/FeedbackModal.tsx +++ b/components/FeedbackModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import Link from 'next/link'; interface FeedbackModalProps { @@ -25,6 +25,14 @@ export default function FeedbackModal({ isOpen, onClose, pageContext }: Feedback const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle'); const [errorMessage, setErrorMessage] = useState(''); + // Lock background scroll while modal is open + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden'; + return () => { document.body.style.overflow = ''; }; + } + }, [isOpen]); + if (!isOpen) return null; const handleSubmit = async (e: React.FormEvent) => { @@ -91,7 +99,7 @@ export default function FeedbackModal({ isOpen, onClose, pageContext }: Feedback return (
{ + document.body.style.overflow = 'hidden'; + return () => { document.body.style.overflow = ''; }; + }, []); + const stats = getStatistics(); const [globalStats, setGlobalStats] = useState(null); const [percentileBeat, setPercentileBeat] = useState(null); @@ -447,9 +453,9 @@ function ResultsModal({ // Percentile: compare user's guess count on THIS puzzle vs this puzzle's distribution if (isWon) { - getPuzzleGuessDistribution(puzzleNumber).then((dist) => { - if (dist) { - const percentile = calculatePuzzlePercentile(guessCount, dist); + getPuzzleGuessDistribution(puzzleNumber).then((result) => { + if (result) { + const percentile = calculatePuzzlePercentile(guessCount, result.distribution, result.totalAttempts); setPercentileBeat(percentile); } }); @@ -527,11 +533,12 @@ function ResultsModal({ return (
e.stopPropagation()} >

@@ -570,7 +577,10 @@ function ResultsModal({ {/* Statistics */}
-

Statistics

+

Statistics

+ {isArchive && ( +

Archive puzzles are not included in your statistics.

+ )}

{stats.gamesPlayed}

diff --git a/components/LegalModals.tsx b/components/LegalModals.tsx index fb75e5c..6bcd2da 100644 --- a/components/LegalModals.tsx +++ b/components/LegalModals.tsx @@ -1,11 +1,19 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; export default function LegalModals() { const [showPrivacyPolicy, setShowPrivacyPolicy] = useState(false); const [showTermsOfService, setShowTermsOfService] = useState(false); + // Lock background scroll while any legal modal is open + useEffect(() => { + if (showPrivacyPolicy || showTermsOfService) { + document.body.style.overflow = 'hidden'; + return () => { document.body.style.overflow = ''; }; + } + }, [showPrivacyPolicy, showTermsOfService]); + return ( <> {/* Legal Section Buttons */} @@ -28,8 +36,8 @@ export default function LegalModals() { {/* Privacy Policy Modal */} {showPrivacyPolicy && ( -
-
+
setShowPrivacyPolicy(false)}> +
e.stopPropagation()}>

Privacy Policy