diff --git a/src/app/__tests__/auth-pages.test.jsx b/src/app/__tests__/auth-pages.test.jsx index eb8db863..62235bd9 100644 --- a/src/app/__tests__/auth-pages.test.jsx +++ b/src/app/__tests__/auth-pages.test.jsx @@ -23,6 +23,8 @@ jest.mock('@clerk/nextjs', () => ({ mockSignIn(props); return
SignIn Component
; }, + ClerkLoading: ({ children }) =>
{children}
, + ClerkLoaded: ({ children }) =>
{children}
, })); // Mock next/navigation diff --git a/src/app/contexts/LoadingContext.jsx b/src/app/contexts/LoadingContext.jsx index 7d8da08b..dbaaac58 100644 --- a/src/app/contexts/LoadingContext.jsx +++ b/src/app/contexts/LoadingContext.jsx @@ -66,16 +66,14 @@ export function LoadingProvider({ children }) { // Clear loading state directly on route changes useEffect(() => { // When pathname changes, clear any loading state immediately - if (isLoading) { - setIsLoading(false); - setMessage(''); - loadingStartTimeRef.current = null; - if (clearTimeoutRef.current) { - clearTimeout(clearTimeoutRef.current); - clearTimeoutRef.current = null; - } + setIsLoading(false); + setMessage(''); + loadingStartTimeRef.current = null; + if (clearTimeoutRef.current) { + clearTimeout(clearTimeoutRef.current); + clearTimeoutRef.current = null; } - }, [pathname, isLoading]); + }, [pathname]); // Cleanup on unmount useEffect(() => { diff --git a/src/app/hooks/__tests__/useBetValidation.test.js b/src/app/hooks/__tests__/useBetValidation.test.js index 8a8bd7bf..89f9c89a 100644 --- a/src/app/hooks/__tests__/useBetValidation.test.js +++ b/src/app/hooks/__tests__/useBetValidation.test.js @@ -7,7 +7,7 @@ describe('useBetValidation', () => { const createScheduledGame = (overrides = {}) => ({ _id: 'game-123', status: GAME_STATUSES.SCHEDULED, - startTime: new Date(Date.now() + 86400000).toISOString(), // Tomorrow + gameDate: new Date(Date.now() + 86400000).toISOString(), // Tomorrow homeTeam: 'Washington Huskies', awayTeam: 'Oregon Ducks', homeOdds: 1.8, @@ -68,19 +68,6 @@ describe('useBetValidation', () => { it('should return canBet: false when game has already started', () => { const { result } = renderHook(() => useBetValidation()); const game = createScheduledGame({ - startTime: new Date(Date.now() - 3600000).toISOString(), // 1 hour ago - }); - - const check = result.current.canBet(game); - - expect(check.canBet).toBe(false); - expect(check.reason).toBe(BETTING_ERRORS.GAME_ALREADY_STARTED); - }); - - it('should use gameDate if startTime is not available', () => { - const { result } = renderHook(() => useBetValidation()); - const game = createScheduledGame({ - startTime: undefined, gameDate: new Date(Date.now() - 3600000).toISOString(), // 1 hour ago }); @@ -90,10 +77,9 @@ describe('useBetValidation', () => { expect(check.reason).toBe(BETTING_ERRORS.GAME_ALREADY_STARTED); }); - it('should return canBet: true when startTime and gameDate are both undefined', () => { + it('should return canBet: true when gameDate is undefined', () => { const { result } = renderHook(() => useBetValidation()); const game = createScheduledGame({ - startTime: undefined, gameDate: undefined, }); @@ -104,10 +90,10 @@ describe('useBetValidation', () => { expect(check.reason).toBeNull(); }); - it('should return canBet: true when startTime is invalid date string', () => { + it('should return canBet: true when gameDate is invalid date string', () => { const { result } = renderHook(() => useBetValidation()); const game = createScheduledGame({ - startTime: 'invalid-date', + gameDate: 'invalid-date', }); const check = result.current.canBet(game); @@ -263,7 +249,7 @@ describe('useBetValidation', () => { const validation = result.current.validate({ ...defaultParams(), game: createScheduledGame({ - startTime: new Date(Date.now() - 3600000).toISOString(), + gameDate: new Date(Date.now() - 3600000).toISOString(), }), }); diff --git a/src/app/hooks/useBetValidation.js b/src/app/hooks/useBetValidation.js index ab01ddc4..84d32556 100644 --- a/src/app/hooks/useBetValidation.js +++ b/src/app/hooks/useBetValidation.js @@ -28,8 +28,8 @@ export function useBetValidation() { /** * Check if a game is available for betting. * Use this for proactive UI disabling (disable bet button before user attempts). - * - * @param {Object} game - Game object with status and startTime + * + * @param {Object} game - Game object with status and gameDate * @returns {{ canBet: boolean, reason: string | null }} */ const canBet = useCallback((game) => { @@ -47,20 +47,17 @@ export function useBetValidation() { return { canBet: false, reason: BETTING_ERRORS.GAME_NOT_SCHEDULED }; } - // Check if game has already started (startTime or gameDate in the past) + // Check if game has already started (gameDate in the past) // Only perform check if we have a valid time value - const gameTimeValue = game.startTime || game.gameDate; - if (gameTimeValue) { - const gameTime = new Date(gameTimeValue); + if (game.gameDate) { + const gameTime = new Date(game.gameDate); const now = new Date(); if (DEBUG) { const isValidDate = !isNaN(gameTime.getTime()); console.log('[useBetValidation] canBet: Date comparison debug', { gameId: game._id, - startTime: game.startTime, gameDate: game.gameDate, - usedValue: gameTimeValue, gameTimeParsed: isValidDate ? gameTime.toISOString() : 'Invalid Date', now: now.toISOString(), isValidDate, @@ -78,7 +75,7 @@ export function useBetValidation() { return { canBet: false, reason: BETTING_ERRORS.GAME_ALREADY_STARTED }; } } else { - if (DEBUG) console.log('[useBetValidation] canBet: no startTime or gameDate available, allowing bet'); + if (DEBUG) console.log('[useBetValidation] canBet: no gameDate available, allowing bet'); } if (DEBUG) console.log('[useBetValidation] canBet: all checks passed, betting allowed'); diff --git a/src/app/login/[[...sign-in]]/page.js b/src/app/login/[[...sign-in]]/page.js index 8ebde92a..53e60b53 100644 --- a/src/app/login/[[...sign-in]]/page.js +++ b/src/app/login/[[...sign-in]]/page.js @@ -1,91 +1,65 @@ 'use client'; -import { SignIn } from '@clerk/nextjs'; +import { SignIn, ClerkLoading, ClerkLoaded } from '@clerk/nextjs'; import { useSearchParams } from 'next/navigation'; import Link from 'next/link'; import { Suspense } from 'react'; +import { HuskyBidsLoader } from '@/components/experimental'; +import { minimalClerkAppearance } from '@/shared/utils/clerk-appearance'; -/** - * Login Page Content - * Extracted to allow Suspense wrapping - */ -function LoginContent() { - const searchParams = useSearchParams(); - const redirectUrl = searchParams.get('redirect') || '/dashboard'; + function LoginContent() { + const searchParams = useSearchParams(); + const redirectUrl = searchParams.get('redirect') || '/dashboard'; - return ( -
- {/* Minimal header */} -
-

- HuskyBids -

-

- Log in to your account -

-
+ return ( +
+ + + - {/* Clerk SignIn Component with minimal styling */} -
- -
+ + {/* Header */} +
+

+ HuskyBids +

+

+ Log in to your account +

+
+ + {/* Clerk SignIn Component */} +
+ +
- {/* Footer link */} -

- No account?{' '} - - Sign up - -

-
- ); -} + {/* Footer link */} +

+ No account?{' '} + + Sign up + +

-/** - * Login Page - * Uses Clerk's pre-built SignIn component for authentication - * Clerk handles redirects natively via afterSignInUrl - */ -export default function LoginPage() { - return ( -
- Loading...
}> - - -
- ); -} + + + ); + } + + export default function LoginPage() { + return ( +
+ Loading...
}> + + + + ); + } \ No newline at end of file diff --git a/src/app/sign-up/[[...sign-up]]/page.jsx b/src/app/sign-up/[[...sign-up]]/page.jsx index 2283c215..93294bbc 100644 --- a/src/app/sign-up/[[...sign-up]]/page.jsx +++ b/src/app/sign-up/[[...sign-up]]/page.jsx @@ -1,8 +1,10 @@ 'use client'; -import { SignUp } from '@clerk/nextjs'; +import { SignUp, ClerkLoading, ClerkLoaded } from '@clerk/nextjs'; import { useSearchParams } from 'next/navigation'; import { Suspense } from 'react'; +import { HuskyBidsLoader } from '@/components/experimental'; +import { minimalClerkAppearance } from '@/shared/utils/clerk-appearance'; /** * Sign Up Page Content @@ -14,54 +16,36 @@ function SignUpContent() { return (
- {/* Minimal header */} -
-

- HuskyBids -

-

- Create your account -

-
- - {/* Clerk SignUp Component with minimal styling */} -
- + -
+ + + {/* Content shown when Clerk is ready */} + + {/* Minimal header */} +
+

+ HuskyBids +

+

+ Create your account +

+
+ + {/* Clerk SignUp Component with minimal styling */} +
+ +
+
); } diff --git a/src/app/sign-up/__tests__/redirect.test.jsx b/src/app/sign-up/__tests__/redirect.test.jsx index f19afdbb..6bb83e20 100644 --- a/src/app/sign-up/__tests__/redirect.test.jsx +++ b/src/app/sign-up/__tests__/redirect.test.jsx @@ -8,6 +8,8 @@ jest.mock('@clerk/nextjs', () => ({ SignUp: (props) => { return
SignUp Component
; }, + ClerkLoading: ({ children }) =>
{children}
, + ClerkLoaded: ({ children }) =>
{children}
, })); // Mock useSearchParams diff --git a/src/app/tasks/page.jsx b/src/app/tasks/page.jsx index e065a828..057532e8 100644 --- a/src/app/tasks/page.jsx +++ b/src/app/tasks/page.jsx @@ -39,6 +39,7 @@ export default function TasksPage() { revalidateOnFocus: false, }); + // Memoize to prevent useEffect from running on every render due to new array reference const tasks = useMemo(() => data?.tasks || [], [data?.tasks]); const summary = data?.summary || null; const streak = data?.streak || null; diff --git a/src/components/experimental/ui/HuskyBidsLoader.jsx b/src/components/experimental/ui/HuskyBidsLoader.jsx index 84c36639..a521afd8 100644 --- a/src/components/experimental/ui/HuskyBidsLoader.jsx +++ b/src/components/experimental/ui/HuskyBidsLoader.jsx @@ -4,7 +4,7 @@ * * @example * - * + * */ 'use client'; @@ -13,6 +13,7 @@ import { cn } from '@/shared/utils'; export default function HuskyBidsLoader({ size = 'md', centered = false, + subtitle = null, className }) { const text = 'HuskyBids'; @@ -33,19 +34,32 @@ export default function HuskyBidsLoader({ className )} > - - {letters.map((letter, index) => ( - - {letter} - - ))} - + {/* Vertical flex container to stack HuskyBids + subtitle */} +
+ + {/* Animated "HuskyBids" text */} + + {letters.map((letter, index) => ( + + {letter} + + ))} + + + {/* Conditionally render subtitle if provided */} + {subtitle && ( +

+ {subtitle} +

+ )} + +
); } diff --git a/src/server/services/BettingService.js b/src/server/services/BettingService.js index 19d9ae27..6335620b 100644 --- a/src/server/services/BettingService.js +++ b/src/server/services/BettingService.js @@ -62,11 +62,11 @@ class BettingService { // Validation: Check if game has started // Use a small buffer (e.g., 5 seconds) to account for slight server time differences const now = new Date(); - const gameTime = new Date(game.gameDate || game.startTime); // Handle both field names + const gameTime = new Date(game.gameDate); const BUFFER_MS = 5000; const cutoffTime = new Date(now.getTime() - BUFFER_MS); - 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()})`); } diff --git a/src/server/services/__tests__/BettingService.test.js b/src/server/services/__tests__/BettingService.test.js index 45cb2e77..4975d9cd 100644 --- a/src/server/services/__tests__/BettingService.test.js +++ b/src/server/services/__tests__/BettingService.test.js @@ -102,6 +102,7 @@ describe('BettingService', () => { _id: 'game123', status: 'scheduled', startTime: new Date(Date.now() + 86400000), // Tomorrow + gameDate: new Date(Date.now() + 86400000), // Synced with startTime homeBets: 0, awayBets: 0, homeBiscuitsWagered: 0, diff --git a/src/shared/utils/clerk-appearance.js b/src/shared/utils/clerk-appearance.js index 296556cc..24d58291 100644 --- a/src/shared/utils/clerk-appearance.js +++ b/src/shared/utils/clerk-appearance.js @@ -12,15 +12,15 @@ export const minimalClerkAppearance = { variables: { - colorPrimary: '#a1a1aa', // zinc-400 + colorPrimary: '#71717a', // zinc-500 (matches login/signup inline config) colorText: '#d4d4d8', // zinc-300 colorTextSecondary: '#71717a', // zinc-500 colorBackground: '#18181b', // zinc-900 colorInputBackground: '#27272a', // zinc-800 colorInputText: '#d4d4d8', // zinc-300 - 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', + fontSize: '14px', + borderRadius: '0px', }, elements: { rootBox: 'mx-auto', @@ -99,6 +99,6 @@ export const minimalClerkAppearance = { }, layout: { socialButtonsPlacement: 'bottom', - socialButtonsVariant: 'blockButton', + socialButtonsVariant: 'iconButton', // Icon buttons (not block) to match original } };