From 6c90af03ce01cfa743050a96d7e3ca3a7ab646dc Mon Sep 17 00:00:00 2001 From: Yonie <164077864+Isaiahriveraa@users.noreply.github.com> Date: Wed, 24 Dec 2025 22:02:51 -0800 Subject: [PATCH 1/6] Add loading states to Clerk auth pages and centralize appearance config 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. --- src/app/__tests__/auth-pages.test.jsx | 2 + src/app/login/[[...sign-in]]/page.js | 136 +++++++----------- src/app/sign-up/[[...sign-up]]/page.jsx | 80 +++++------ src/app/sign-up/__tests__/redirect.test.jsx | 2 + .../experimental/ui/HuskyBidsLoader.jsx | 42 ++++-- src/shared/utils/clerk-appearance.js | 10 +- 6 files changed, 124 insertions(+), 148 deletions(-) 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/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/components/experimental/ui/HuskyBidsLoader.jsx b/src/components/experimental/ui/HuskyBidsLoader.jsx index 84c36639..24bc2f06 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, // TODO: New prop added - defaults to 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/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 } }; From 8d84073588d5ffb7ec0b79652525d548bb56f181 Mon Sep 17 00:00:00 2001 From: Yonie <164077864+Isaiahriveraa@users.noreply.github.com> Date: Wed, 24 Dec 2025 22:07:00 -0800 Subject: [PATCH 2/6] Fix React dependency bug and remove non-existent field reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- src/app/contexts/LoadingContext.jsx | 2 +- src/server/services/BettingService.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/contexts/LoadingContext.jsx b/src/app/contexts/LoadingContext.jsx index 7d8da08b..012fe0bd 100644 --- a/src/app/contexts/LoadingContext.jsx +++ b/src/app/contexts/LoadingContext.jsx @@ -75,7 +75,7 @@ export function LoadingProvider({ children }) { clearTimeoutRef.current = null; } } - }, [pathname, isLoading]); + }, [pathname]); // Cleanup on unmount useEffect(() => { diff --git a/src/server/services/BettingService.js b/src/server/services/BettingService.js index 19d9ae27..082f003f 100644 --- a/src/server/services/BettingService.js +++ b/src/server/services/BettingService.js @@ -62,7 +62,7 @@ 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); From afdca637a0c2939fcb5fcb4fce60f3f1c0f05710 Mon Sep 17 00:00:00 2001 From: Yonie <164077864+Isaiahriveraa@users.noreply.github.com> Date: Wed, 24 Dec 2025 22:36:31 -0800 Subject: [PATCH 3/6] Standardized betting validation to use gameDate and improved service 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. --- .../hooks/__tests__/useBetValidation.test.js | 24 ++++--------------- src/app/hooks/useBetValidation.js | 15 +++++------- src/server/services/BettingService.js | 2 +- .../services/__tests__/BettingService.test.js | 1 + 4 files changed, 13 insertions(+), 29 deletions(-) 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/server/services/BettingService.js b/src/server/services/BettingService.js index 082f003f..6335620b 100644 --- a/src/server/services/BettingService.js +++ b/src/server/services/BettingService.js @@ -66,7 +66,7 @@ class BettingService { 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, From c5ba2cb9d069168d34708378f54aa2a279e0fc2f Mon Sep 17 00:00:00 2001 From: Yonie <164077864+Isaiahriveraa@users.noreply.github.com> Date: Wed, 24 Dec 2025 22:36:55 -0800 Subject: [PATCH 4/6] Simplified loading state cleanup on route changes 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. --- src/app/contexts/LoadingContext.jsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/app/contexts/LoadingContext.jsx b/src/app/contexts/LoadingContext.jsx index 012fe0bd..dbaaac58 100644 --- a/src/app/contexts/LoadingContext.jsx +++ b/src/app/contexts/LoadingContext.jsx @@ -66,14 +66,12 @@ 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]); From 2087bbbde294cc853aa63ac2d33dec5c88d23a0f Mon Sep 17 00:00:00 2001 From: Yonie <164077864+Isaiahriveraa@users.noreply.github.com> Date: Wed, 24 Dec 2025 22:37:08 -0800 Subject: [PATCH 5/6] Optimized Tasks page rendering with memoized data 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. --- src/app/tasks/page.jsx | 1 + 1 file changed, 1 insertion(+) 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; From cd8574f9c0339a0f515a773d05c17e08dbc36882 Mon Sep 17 00:00:00 2001 From: Yonie <164077864+Isaiahriveraa@users.noreply.github.com> Date: Wed, 24 Dec 2025 22:58:07 -0800 Subject: [PATCH 6/6] Update src/components/experimental/ui/HuskyBidsLoader.jsx Remove redundant comment. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/components/experimental/ui/HuskyBidsLoader.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/experimental/ui/HuskyBidsLoader.jsx b/src/components/experimental/ui/HuskyBidsLoader.jsx index 24bc2f06..a521afd8 100644 --- a/src/components/experimental/ui/HuskyBidsLoader.jsx +++ b/src/components/experimental/ui/HuskyBidsLoader.jsx @@ -13,7 +13,7 @@ import { cn } from '@/shared/utils'; export default function HuskyBidsLoader({ size = 'md', centered = false, - subtitle = null, // TODO: New prop added - defaults to null + subtitle = null, className }) { const text = 'HuskyBids';