Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion __tests__/unit/supabase.pure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> = {}): Puzzle {
Expand Down Expand Up @@ -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)
})
})
2 changes: 1 addition & 1 deletion app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export default function AboutPage() {
</p>
<div className="flex flex-col sm:flex-row gap-4 mt-6">
<a
href="https://github.com/kganiga/radiordle"
href="https://github.com/kishanasokan/Radwordle"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 px-6 py-3 bg-[#24292e] hover:bg-[#3a3f44] text-white rounded-lg transition-colors font-bold"
Expand Down
10 changes: 5 additions & 5 deletions app/archive/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import ArchiveBrowser from '@/components/ArchiveBrowser';

export default function ArchivePage() {
return (
<div className="min-h-screen relative overflow-y-auto overflow-x-hidden">
<div className="min-h-screen-safe relative overflow-y-auto overflow-x-hidden" style={{ minHeight: 'var(--full-vh)' }}>
{/* Gradient Background */}
<div className="absolute inset-0 bg-gradient-to-br from-page-bg via-page-bg-mid to-page-bg">
{/* Radial Vignette */}
Expand All @@ -18,7 +18,7 @@ export default function ArchivePage() {
</div>

{/* Content */}
<div className="relative z-10 min-h-screen flex flex-col">
<div className="relative z-10 min-h-screen-safe flex flex-col" style={{ minHeight: 'var(--full-vh)' }}>
{/* Header */}
<div className="flex justify-between items-center p-4 sm:p-6">
{/* Back Button */}
Expand All @@ -29,8 +29,8 @@ export default function ArchivePage() {
<span className="text-xl sm:text-2xl">←</span>
</Link>

{/* Logo and Title - Centered */}
<div className="flex items-center gap-1 drop-shadow-[0_6px_20px_rgba(0,0,0,0.6)]">
{/* Logo and Title - Centered, clickable to return home */}
<Link href="/" className="flex items-center gap-1 drop-shadow-[0_6px_20px_rgba(0,0,0,0.6)] hover:opacity-90 transition-opacity">
<div className="relative w-10 h-10 sm:w-14 sm:h-14 md:w-16 md:h-16 flex-shrink-0">
<Image
src="/radle_icon.svg"
Expand All @@ -42,7 +42,7 @@ export default function ArchivePage() {
<h1 className="text-2xl sm:text-4xl md:text-[3.375rem] text-white font-baloo-2 font-extrabold tracking-tight">
Radiordle
</h1>
</div>
</Link>

{/* Spacer for centering */}
<div className="w-10 sm:w-12"></div>
Expand Down
12 changes: 10 additions & 2 deletions components/FeedbackModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useState } from 'react';
import { useState, useEffect } from 'react';
import Link from 'next/link';

interface FeedbackModalProps {
Expand All @@ -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) => {
Expand Down Expand Up @@ -91,7 +99,7 @@ export default function FeedbackModal({ isOpen, onClose, pageContext }: Feedback

return (
<div
className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-[100] p-4 animate-backdrop-fade"
className="fixed inset-0 bg-black/50 flex items-center justify-center z-[100] p-4 animate-backdrop-fade"
onClick={handleClose}
>
<div
Expand Down
22 changes: 16 additions & 6 deletions components/GameClient.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { useState, useCallback, useEffect, useRef } from 'react';
import { Condition, getGlobalStats, calculatePercentileBeat, getPuzzleGuessDistribution, calculatePuzzlePercentile, GlobalStats } from '@/lib/supabase';

Check warning on line 4 in components/GameClient.tsx

View workflow job for this annotation

GitHub Actions / Lint & Type Check

'calculatePercentileBeat' is defined but never used
import { getCookieConsent } from './CookieConsent';
import DiagnosisAutocomplete from './DiagnosisAutocomplete';
import { checkAnswer } from '@/lib/gameLogic';
Expand Down Expand Up @@ -434,6 +434,12 @@
onClose,
onCopied,
}: ResultsModalProps) {
// Lock background scroll while modal is open
useEffect(() => {
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = ''; };
}, []);

const stats = getStatistics();
const [globalStats, setGlobalStats] = useState<GlobalStats | null>(null);
const [percentileBeat, setPercentileBeat] = useState<number | null>(null);
Expand All @@ -447,9 +453,9 @@

// 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);
}
});
Expand Down Expand Up @@ -527,11 +533,12 @@

return (
<div
className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4 animate-backdrop-fade"
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 animate-backdrop-fade"
data-testid="results-modal"
onClick={onClose}
>
<div
className="bg-gradient-to-b from-modal-bg to-page-bg-dark rounded-lg p-4 sm:p-8 max-w-md sm:max-w-2xl w-full shadow-2xl max-h-[90vh] overflow-y-auto font-baloo-2 animate-modal-enter"
className="bg-gradient-to-b from-modal-bg to-page-bg-dark rounded-lg p-4 sm:p-8 max-w-md sm:max-w-2xl w-full shadow-2xl max-h-[90vh] sm:max-h-none overflow-y-auto font-baloo-2 animate-modal-enter"
onClick={(e) => e.stopPropagation()}
>
<h2 className="text-2xl sm:text-3xl font-bold text-white text-center mb-2 sm:mb-4">
Expand Down Expand Up @@ -570,7 +577,10 @@

{/* Statistics */}
<div className="bg-white rounded-lg p-3 sm:p-4 mb-2 sm:mb-4">
<h3 className="text-xl sm:text-2xl font-bold text-black text-center mb-2">Statistics</h3>
<h3 className="text-xl sm:text-2xl font-bold text-black text-center mb-1">Statistics</h3>
{isArchive && (
<p className="text-xs italic text-gray-500 text-center mb-2">Archive puzzles are not included in your statistics.</p>
)}
<div className="grid grid-cols-4 gap-2 sm:gap-3 text-center">
<div>
<p className="text-xl sm:text-2xl font-bold text-success leading-tight">{stats.gamesPlayed}</p>
Expand Down
18 changes: 13 additions & 5 deletions components/LegalModals.tsx
Original file line number Diff line number Diff line change
@@ -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 */}
Expand All @@ -28,8 +36,8 @@ export default function LegalModals() {

{/* Privacy Policy Modal */}
{showPrivacyPolicy && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70">
<div className="bg-page-bg rounded-xl w-full max-w-2xl max-h-[85vh] overflow-hidden shadow-2xl border border-white/10">
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50" onClick={() => setShowPrivacyPolicy(false)}>
<div className="bg-page-bg rounded-xl w-full max-w-2xl max-h-[85vh] overflow-hidden shadow-2xl border border-white/10" onClick={(e) => e.stopPropagation()}>
<div className="flex justify-between items-center p-4 sm:p-6 border-b border-white/10">
<h2 className="text-xl sm:text-2xl text-white font-baloo-2 font-bold">Privacy Policy</h2>
<button
Expand Down Expand Up @@ -110,8 +118,8 @@ export default function LegalModals() {

{/* Terms of Service Modal */}
{showTermsOfService && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70">
<div className="bg-page-bg rounded-xl w-full max-w-2xl max-h-[85vh] overflow-hidden shadow-2xl border border-white/10">
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50" onClick={() => setShowTermsOfService(false)}>
<div className="bg-page-bg rounded-xl w-full max-w-2xl max-h-[85vh] overflow-hidden shadow-2xl border border-white/10" onClick={(e) => e.stopPropagation()}>
<div className="flex justify-between items-center p-4 sm:p-6 border-b border-white/10">
<h2 className="text-xl sm:text-2xl text-white font-baloo-2 font-bold">Terms of Service</h2>
<button
Expand Down
12 changes: 10 additions & 2 deletions components/StatsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ export default function StatsModal({ isOpen, onClose, stats }: StatsModalProps)
const [globalStats, setGlobalStats] = useState<GlobalStats | null>(null);
const [percentileBeat, setPercentileBeat] = useState<number | null>(null);

// Lock background scroll while modal is open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = ''; };
}
}, [isOpen]);

// Fetch global stats when modal opens
useEffect(() => {
if (isOpen) {
Expand Down Expand Up @@ -61,11 +69,11 @@ export default function StatsModal({ isOpen, onClose, stats }: StatsModalProps)

return (
<div
className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-[100] p-4 animate-backdrop-fade"
className="fixed inset-0 bg-black/50 flex items-center justify-center z-[100] p-4 animate-backdrop-fade"
onClick={onClose}
>
<div
className="bg-gradient-to-b from-modal-bg to-page-bg-dark rounded-lg p-4 sm:p-8 max-w-md w-full shadow-2xl max-h-[90vh] overflow-y-auto font-baloo-2 animate-modal-enter"
className="bg-gradient-to-b from-modal-bg to-page-bg-dark rounded-lg p-4 sm:p-8 max-w-md w-full shadow-2xl max-h-[90vh] sm:max-h-none overflow-y-auto font-baloo-2 animate-modal-enter"
onClick={(e) => e.stopPropagation()}
>
{/* Title */}
Expand Down
4 changes: 2 additions & 2 deletions e2e/fixtures/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Page, expect, Locator } from '@playwright/test';

Check warning on line 1 in e2e/fixtures/helpers.ts

View workflow job for this annotation

GitHub Actions / Lint & Type Check

'expect' is defined but never used

/**
* The game renders dual layouts (desktop + mobile) so many elements appear twice.
Expand Down Expand Up @@ -236,8 +236,8 @@
* Waits for the game to fully load (image and input visible).
*/
export async function waitForGameLoad(page: Page): Promise<void> {
// Wait for the game image
await page.locator('img[alt*="Puzzle"]').waitFor({ state: 'visible', timeout: 15000 });
// Wait for the game image (use .first() since annotated image overlay may also match)
await page.locator('img[alt*="Puzzle"]').first().waitFor({ state: 'visible', timeout: 15000 });
// Wait for a visible input field (uses :visible to handle dual layout on any viewport)
await page.locator('input[placeholder="Diagnosis..."]:visible').first().waitFor({ state: 'visible', timeout: 5000 });
}
2 changes: 1 addition & 1 deletion e2e/tests/first-time-user.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ test.describe('First-Time User Journey', () => {

// The results modal (inline in GameClient) should show stats
// Check for statistics section in the modal
const modal = page.locator('.fixed.inset-0.bg-black');
const modal = page.locator('[data-testid="results-modal"]');
await expect(modal.getByText('Statistics')).toBeVisible();

// Verify stats: 1 game played
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/guess-time-tracking.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ test.describe('Guess Time Tracking', () => {
await expect(page.getByText('Congratulations!').first()).toBeVisible({ timeout: 3000 });

// The results modal should show average guess time
const modal = page.locator('.fixed.inset-0.bg-black');
const modal = page.locator('[data-testid="results-modal"]');

// Look for the "Avg Time" label — use .first() since desktop+mobile layouts both render it
const avgTimeLabel = modal.getByText('Avg Time').first();
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/losing-game.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ test.describe('Losing Game Flow', () => {
// Modal should be visible with stats
await expect(page.getByText('Game Over').first()).toBeVisible({ timeout: 3000 });

const modal = page.locator('.fixed.inset-0.bg-black');
const modal = page.locator('[data-testid="results-modal"]');

// Verify stats: 1 game played, 0% win rate
const playedStat = modal.locator('text=Played').locator('..');
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/mobile-responsive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ test.describe('Mobile Responsiveness', () => {
await waitForGameLoad(page);

// Image should be visible and fit within viewport
const image = page.locator('img[alt*="Puzzle"]');
const image = page.locator('img[alt*="Puzzle"]').first();
const box = await image.boundingBox();
expect(box).not.toBeNull();
if (box) {
Expand Down
6 changes: 3 additions & 3 deletions e2e/tests/network-failure.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ test.describe('Network Failure Handling', () => {

// Wait for game to load
try {
await page.locator('img[alt*="Puzzle"]').waitFor({ state: 'visible', timeout: 15000 });
await page.locator('img[alt*="Puzzle"]').first().waitFor({ state: 'visible', timeout: 15000 });
} catch {
// If the page fails to load, skip this test
test.skip();
Expand Down Expand Up @@ -95,7 +95,7 @@ test.describe('Network Failure Handling', () => {
await acceptCookieConsent(page);

try {
await page.locator('img[alt*="Puzzle"]').waitFor({ state: 'visible', timeout: 15000 });
await page.locator('img[alt*="Puzzle"]').first().waitFor({ state: 'visible', timeout: 15000 });
} catch {
test.skip();
return;
Expand All @@ -113,7 +113,7 @@ test.describe('Network Failure Handling', () => {
await page.reload();

try {
await page.locator('img[alt*="Puzzle"]').waitFor({ state: 'visible', timeout: 15000 });
await page.locator('img[alt*="Puzzle"]').first().waitFor({ state: 'visible', timeout: 15000 });
// Game loaded successfully after network restored
await expect(page.getByText("What's the Diagnosis?").first()).toBeVisible();
} catch {
Expand Down
Loading
Loading