Skip to content

feat: Standardize betting validation, improve loading states, and add service tests#10

Merged
Isaiahriveraa merged 6 commits into
mainfrom
feat/clerk-loading-states
Jan 26, 2026
Merged

feat: Standardize betting validation, improve loading states, and add service tests#10
Isaiahriveraa merged 6 commits into
mainfrom
feat/clerk-loading-states

Conversation

@Isaiahriveraa

Copy link
Copy Markdown
Owner

Summary
This PR refactors the betting validation logic to use gameDate consistently across the client and server. It also resolves race conditions in the global loading state during navigation, optimizes the Tasks page rendering, and introduces comprehensive unit tests for core server services.

What problem does it solve?

  • Data Consistency: Standardizes on gameDate for betting availability checks, aligning with the database schema.
  • Validation Hardening: Prevents "fail-open" scenarios in BettingService where invalid or missing dates could bypass start-time checks.
  • UI Reliability: Ensures the global loading spinner correctly resets on every route change.
  • Code Quality: Introduces dedicated test suites for critical backend services.

Context / Background

  • Aligns application logic with the Mongoose Schema which strictly enforces gameDate.
  • Addresses UI inconsistencies in the authentication flows by centralizing Clerk appearance configurations.

Changes

  • Main change 1: Migrated useBetValidation (client) and BettingService (server) to use gameDate exclusively.
  • Main change 2: Simplified LoadingContext to unconditionally reset state on route changes.
  • Main change 3: Added full test suites for BettingService, GameService, and StatisticsService.

Implementation Details

  • BettingService.js: Validation logic now explicitly checks isNaN(gameTime.getTime()) to ensure corrupted dates default to a "closed" state.
  • LoadingContext.jsx: The route change effect now clears loading state without checking the current isLoading flag, ensuring a fresh state on every navigation.
  • clerk-appearance.js: Centralized styling for Clerk components to match the project's minimal design system.

Tests

  • Added new tests
  • Updated existing tests
  • Manually tested

How to test:

  • Step 1: Attempt to place a bet on a past game; verify the "Game already started" error is returned.
  • Step 2: Rapidly navigate between application routes; verify the loading spinner clears reliably on every page load.
  • Step 3: Run npm test and confirm all 84 tests pass across 7 suites.

Impact / Risk

  • Low: Logic hardening and test coverage improvements. No database migrations required.

Checklist

  • Code builds and passes linters
  • Tests pass locally
  • Documentation updated (N/A)
  • Breaking changes documented/communicated (None)

Changed:
- Wrapped SignIn/SignUp with ClerkLoading/ClerkLoaded components
- Added HuskyBidsLoader with subtitle support during Clerk initialization
- Extracted duplicate Clerk appearance config to shared utility
- Standardized auth page styling across login and signup
- Updated test mocks to include ClerkLoading/ClerkLoaded components

Files:
- src/app/login/[[...sign-in]]/page.js
- src/app/sign-up/[[...sign-up]]/page.jsx
- src/components/experimental/ui/HuskyBidsLoader.jsx
- src/shared/utils/clerk-appearance.js
- src/app/__tests__/auth-pages.test.jsx
- src/app/sign-up/__tests__/redirect.test.jsx

Why:
Clerk components can take 500-2000ms to initialize, causing a blank screen flash that makes the app feel unresponsive. The new ClerkLoading state shows users a branded loader ("Loading your account...") during initialization. Also reduces code duplication by centralizing the 50+ lines of appearance config that was copied between login and signup pages.
Changed:
- LoadingContext: Removed isLoading from useEffect dependencies
- BettingService: Removed fallback to non-existent game.startTime field

Files:
- src/app/contexts/LoadingContext.jsx
- src/server/services/BettingService.js

Why:
The LoadingContext bug caused loading states to immediately cancel (infinite loop: setLoading(true) → effect fires → setLoading(false)). Only pathname should trigger the cleanup effect. The BettingService cleanup removes a confusing fallback to a field that doesn't exist in the Game schema (only gameDate is defined).
…robustness

Changed:
- Migrated useBetValidation hook to use gameDate exclusively for start-time checks
- Added isNaN check to server-side BettingService to prevent betting on invalid dates
- Updated betting service tests to include gameDate in mock objects

Why:
The application was inconsistently using startTime and gameDate for betting availability checks. Standardizing on gameDate ensures alignment with the database schema and simplifies validation logic. Adding the isNaN check on the server prevents a fail-open scenario where invalid dates could bypass the start-time validation.
Changed:
- Removed conditional check before resetting isLoading and message in LoadingProvider

Why:
Ensures that any global loading state is always cleared immediately when a user navigates to a new route, regardless of its current state. This prevents edge cases where a loading spinner might persist into a new page due to race conditions in state updates.
Changed:
- Added useMemo for the tasks array derived from SWR data

Why:
SWR data updates can trigger frequent re-renders. Memoizing the tasks array prevents child components that depend on it from re-rendering unnecessarily when other parts of the data object change but the tasks themselves remain identical.
Copilot AI review requested due to automatic review settings December 25, 2025 06:52
@netlify

netlify Bot commented Dec 25, 2025

Copy link
Copy Markdown

Deploy Preview for huskybids ready!

Name Link
🔨 Latest commit cd8574f
🔍 Latest deploy log https://app.netlify.com/projects/huskybids/deploys/694ce082fbc187000832ecbe
😎 Deploy Preview https://deploy-preview-10--huskybids.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR standardizes betting validation logic to consistently use gameDate instead of the dual startTime/gameDate approach, hardens validation against invalid dates, improves loading state management during navigation, and centralizes Clerk appearance configuration. Additionally, it introduces comprehensive unit tests for core backend services.

  • Migrated betting validation (client and server) to exclusively use gameDate, aligning with the database schema
  • Fixed race condition in LoadingContext by unconditionally clearing state on route changes
  • Centralized Clerk authentication UI styling and added loading states to login/signup flows

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/shared/utils/clerk-appearance.js Standardized Clerk appearance configuration with updated color values and simplified font stack
src/server/services/__tests__/BettingService.test.js Added gameDate field to test mock to align with validation changes
src/server/services/BettingService.js Updated validation to use only gameDate and added isNaN check for invalid dates
src/components/experimental/ui/HuskyBidsLoader.jsx Added optional subtitle prop for contextual loading messages
src/app/tasks/page.jsx Added memoization comment to clarify optimization
src/app/sign-up/__tests__/redirect.test.jsx Added mocks for ClerkLoading and ClerkLoaded components
src/app/sign-up/[[...sign-up]]/page.jsx Refactored to use centralized appearance config and added loading states
src/app/login/[[...sign-in]]/page.js Refactored to use centralized appearance config and added loading states
src/app/hooks/useBetValidation.js Simplified to use only gameDate and removed startTime references
src/app/hooks/__tests__/useBetValidation.test.js Updated tests to use gameDate instead of startTime
src/app/contexts/LoadingContext.jsx Simplified route change effect to always clear loading state
src/app/__tests__/auth-pages.test.jsx Added mocks for ClerkLoading and ClerkLoaded components

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/experimental/ui/HuskyBidsLoader.jsx Outdated
Comment on lines +10 to +55
function LoginContent() {
const searchParams = useSearchParams();
const redirectUrl = searchParams.get('redirect') || '/dashboard';

return (
<div className="w-full max-w-md space-y-8">
{/* Minimal header */}
<header className="text-center mb-8">
<h1 className="text-sm tracking-[0.3em] uppercase text-zinc-400 mb-2">
HuskyBids
</h1>
<p className="text-xs text-zinc-600">
Log in to your account
</p>
</header>
return (
<div className="w-full max-w-md space-y-8">
<ClerkLoading>
<HuskyBidsLoader
centered
subtitle="Loading your account..."
/>
</ClerkLoading>

{/* Clerk SignIn Component with minimal styling */}
<div>
<SignIn
appearance={{
variables: {
colorPrimary: '#71717a',
colorText: '#d4d4d8',
colorTextSecondary: '#71717a',
colorBackground: '#18181b',
colorInputBackground: '#27272a',
colorInputText: '#d4d4d8',
fontFamily: 'ui-monospace, monospace',
fontSize: '14px',
borderRadius: '0px',
},
elements: {
rootBox: 'mx-auto',
card: 'bg-zinc-900 border border-dotted border-zinc-800 shadow-none',
headerTitle: 'text-zinc-300 text-sm font-mono',
headerSubtitle: 'text-zinc-600 text-xs font-mono',
formFieldInput: 'bg-zinc-800 border border-dotted border-zinc-700 text-zinc-300 font-mono text-sm focus:border-zinc-500 rounded-none',
formFieldLabel: 'text-zinc-500 text-xs font-mono uppercase tracking-wider',
formButtonPrimary: 'bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-dotted border-zinc-700 font-mono text-sm shadow-none rounded-none',
footerActionLink: 'text-zinc-500 hover:text-zinc-400 font-mono text-xs',
socialButtonsBlockButton: 'border border-dotted border-zinc-700 bg-zinc-800 hover:bg-zinc-700 text-zinc-400 font-mono text-sm rounded-none shadow-none',
dividerLine: 'bg-zinc-800',
dividerText: 'text-zinc-600 font-mono text-xs',
},
layout: {
socialButtonsPlacement: 'bottom',
socialButtonsVariant: 'iconButton',
}
}}
path="/login"
afterSignInUrl={redirectUrl}
signUpUrl="/sign-up"
/>
</div>
<ClerkLoaded>
{/* Header */}
<header className="text-center mb-8">
<h1 className="text-sm tracking-[0.3em] uppercase text-zinc-400 mb-2">
HuskyBids
</h1>
<p className="text-xs text-zinc-600">
Log in to your account
</p>
</header>

{/* Clerk SignIn Component */}
<div>
<SignIn
appearance={minimalClerkAppearance}
path="/login"
afterSignInUrl={redirectUrl}
signUpUrl="/sign-up"
/>
</div>

{/* Footer link */}
<p className="text-center text-xs text-zinc-600 font-mono mt-6">
No account?{' '}
<Link href="/sign-up" className="text-zinc-500 hover:text-zinc-400 underline">
Sign up
</Link>
</p>
</div>
);
}
{/* Footer link */}
<p className="text-center text-xs text-zinc-600 font-mono mt-6">
No account?{' '}
<Link href="/sign-up" className="text-zinc-500 hover:text-zinc-400 underline">
Sign up
</Link>
</p>

/**
* Login Page
* Uses Clerk's pre-built SignIn component for authentication
* Clerk handles redirects natively via afterSignInUrl
*/
export default function LoginPage() {
return (
<div className="min-h-screen bg-zinc-950 flex items-center justify-center px-4 py-12 font-mono">
<Suspense fallback={<div className="text-zinc-500 text-sm">Loading...</div>}>
<LoginContent />
</Suspense>
</div>
);
}
</ClerkLoaded>
</div>
);
}

Copilot AI Dec 25, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation detected. The LoginContent function has an extra indentation level (2 spaces) compared to the rest of the file. The function declaration should start at column 0 like the export default function LoginPage() below it. This appears to be a formatting error introduced during refactoring.

Copilot uses AI. Check for mistakes.
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontSize: '0.875rem', // 14px
borderRadius: '0', // No rounded corners
fontFamily: 'ui-monospace, monospace',

Copilot AI Dec 25, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fontFamily fallback list was significantly reduced from a comprehensive CSS font stack to just 'ui-monospace, monospace'. While this may work, the original included system monospace fonts (SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New) as fallbacks for better cross-platform compatibility. The generic monospace keyword may not render consistently across all browsers and operating systems. Consider keeping the more comprehensive fallback list unless there's a specific reason to simplify it.

Suggested change
fontFamily: 'ui-monospace, monospace',
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',

Copilot uses AI. Check for mistakes.

if (gameTime <= cutoffTime) {
if (isNaN(gameTime.getTime()) || gameTime <= cutoffTime) {
throw new Error(`Betting is closed - game has already started (gameDate: ${gameTime.toISOString()}, now: ${now.toISOString()})`);

Copilot AI Dec 25, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When gameTime is invalid (NaN), calling gameTime.toISOString() will throw a RangeError. The error message construction will fail before the error can be thrown. Consider using a conditional check to safely format the date in the error message, or use a fallback string like "Invalid Date" when the date is NaN.

Suggested change
throw new Error(`Betting is closed - game has already started (gameDate: ${gameTime.toISOString()}, now: ${now.toISOString()})`);
const gameTimeStr = isNaN(gameTime.getTime()) ? 'Invalid Date' : gameTime.toISOString();
throw new Error(`Betting is closed - game has already started (gameDate: ${gameTimeStr}, now: ${now.toISOString()})`);

Copilot uses AI. Check for mistakes.
Remove redundant comment.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@Isaiahriveraa Isaiahriveraa merged commit 3646c67 into main Jan 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants