Skip to content

Commit 1d1bb7a

Browse files
feat: Implement Global Loading State with LoadingContext (#1507)
Refactor: Align with Next.js 16 Component Architecture (#1505) feat: refactor layout for Next.js 16 client/server boundaries Refactors the root layout to align with Next.js 16's stricter client/server component architecture. - Creates a new `app/main.tsx` client component to consolidate all client-side providers. - Simplifies `app/layout.tsx` to a pure server component, responsible only for the root HTML structure and metadata. - Updates `components/Providers.tsx` to remove redundant theme providers. This change resolves build failures related to metadata exports from client components and establishes a clear, maintainable pattern for the application's provider hierarchy. chore: update visual regression snapshots Updates the Playwright visual regression snapshots to reflect the changes introduced by the architectural refactor. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> refactor: Address PR feedback for LoadingIndicator and theme This commit addresses the feedback from the pull request review: - **`app/layout.tsx`**: Removed the unnecessary comment next to the `Main` component import. - **`lib/theme.ts`**: - Added a `loadingIndicator` level to `theme.zIndex`. - Added an `overlay` color to `theme.palette.background`. - Added TypeScript module declarations for the new custom theme properties to ensure type safety. - **`components/LoadingIndicator.tsx`**: - Refactored the component to use the new `zIndex` and `backgroundColor` values from the theme. - Implemented a smooth fade-in/fade-out transition using `opacity` and `visibility` properties, making the UX smoother as requested. fix: Run `lint --fix` to correct formatting This commit fixes all linting errors reported in the PR feedback. - Ran `pnpm run lint:fix` to automatically correct Prettier formatting issues across multiple files. - Manually verified and removed a false-positive `eslint-disable` directive in `hooks/useWorkoutSession.ts`. All linting checks now pass. fix: Correct TypeScript errors in theme and remove unused imports This commit fixes the build failures identified in the PR feedback. - **`lib/theme.ts`**: - Corrected the MUI module declaration to properly augment the `TypeBackground` interface, resolving the TypeScript conflict. - Fixed a syntax error in the `shadows` array. - **`components/LoadingIndicator.tsx`**: - Removed the unused `React` import, which was causing a build failure. The application now builds successfully. fix: Update LoadingIndicator test to reflect new implementation This commit fixes the failing unit test for the `LoadingIndicator` component. The test was updated to assert that the progressbar is not present in the DOM when `isLoading` is false, instead of checking for a `null` container. This aligns the test with the component's new behavior of using `opacity` and `visibility` for transitions. All unit tests are now passing. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent 89c631a commit 1d1bb7a

6 files changed

Lines changed: 179 additions & 8 deletions

File tree

app/main.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,29 @@ import ErrorBoundary from '@/components/ErrorBoundary'
55
import ErrorDisplay from '@/components/ErrorDisplay'
66
import ErrorFallback from '@/components/ErrorFallback'
77
import Footer from '@/components/Footer'
8+
import LoadingIndicator from '@/components/LoadingIndicator'
89
import Providers from '@/components/Providers'
910
import ThemeRegistry from '@/components/ThemeRegistry/ThemeRegistry'
1011
import TimerSoundProvider from '@/components/TimerSoundProvider'
1112
import { ErrorProvider } from '@/context/ErrorContext'
13+
import { LoadingProvider } from '@/context/LoadingContext'
1214
import { UserSettingsProvider } from '@/context/UserSettingsContext'
1315

1416
export default function Main({ children }: { children: React.ReactNode }) {
1517
return (
1618
<ThemeRegistry options={{ key: 'mui' }}>
1719
<ErrorProvider>
18-
<Providers>
19-
<UserSettingsProvider>
20-
<ErrorBoundary fallback={<ErrorFallback />}>
21-
<TimerSoundProvider>{children}</TimerSoundProvider>
22-
</ErrorBoundary>
23-
</UserSettingsProvider>
24-
</Providers>
25-
<ErrorDisplay />
20+
<LoadingProvider>
21+
<Providers>
22+
<UserSettingsProvider>
23+
<ErrorBoundary fallback={<ErrorFallback />}>
24+
<TimerSoundProvider>{children}</TimerSoundProvider>
25+
</ErrorBoundary>
26+
</UserSettingsProvider>
27+
</Providers>
28+
<LoadingIndicator />
29+
<ErrorDisplay />
30+
</LoadingProvider>
2631
</ErrorProvider>
2732
<Footer />
2833
<BottomNavBar />

components/LoadingIndicator.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use client'
2+
3+
import { useLoading } from '@/context/LoadingContext'
4+
import { Box, CircularProgress, useTheme } from '@mui/material'
5+
6+
const LoadingIndicator = () => {
7+
const { isLoading } = useLoading()
8+
const theme = useTheme()
9+
10+
return (
11+
<Box
12+
sx={{
13+
position: 'fixed',
14+
top: 0,
15+
left: 0,
16+
width: '100%',
17+
height: '100%',
18+
display: 'flex',
19+
justifyContent: 'center',
20+
alignItems: 'center',
21+
backgroundColor: theme.palette.background.overlay,
22+
zIndex: theme.zIndex.loadingIndicator,
23+
transition: theme.transitions.create('opacity', {
24+
duration: theme.transitions.duration.short,
25+
}),
26+
opacity: isLoading ? 1 : 0,
27+
visibility: isLoading ? 'visible' : 'hidden',
28+
}}
29+
>
30+
<CircularProgress />
31+
</Box>
32+
)
33+
}
34+
35+
export default LoadingIndicator

context/LoadingContext.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use client'
2+
3+
import { createContext, useContext, useState, ReactNode } from 'react'
4+
5+
interface LoadingContextType {
6+
isLoading: boolean
7+
setIsLoading: (isLoading: boolean) => void
8+
}
9+
10+
const LoadingContext = createContext<LoadingContextType | undefined>(undefined)
11+
12+
export const LoadingProvider = ({ children }: { children: ReactNode }) => {
13+
const [isLoading, setIsLoading] = useState(false)
14+
15+
return (
16+
<LoadingContext.Provider value={{ isLoading, setIsLoading }}>
17+
{children}
18+
</LoadingContext.Provider>
19+
)
20+
}
21+
22+
export const useLoading = () => {
23+
const context = useContext(LoadingContext)
24+
if (context === undefined) {
25+
throw new Error('useLoading must be used within a LoadingProvider')
26+
}
27+
return context
28+
}

lib/theme.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
import { createTheme } from '@mui/material/styles'
44

5+
// Extend the MUI theme types to include custom properties
6+
declare module '@mui/material/styles' {
7+
interface ZIndex {
8+
loadingIndicator: number
9+
}
10+
interface TypeBackground {
11+
overlay: string
12+
}
13+
}
14+
515
/**
616
* HRM Application Design System
717
*
@@ -52,6 +62,7 @@ const theme = createTheme({
5262
background: {
5363
default: '#F5F5F5', // Light grey for main background
5464
paper: '#FFFFFF',
65+
overlay: 'rgba(0, 0, 0, 0.5)', // Added for loading indicator
5566
},
5667
// Text colors
5768
text: {
@@ -162,6 +173,16 @@ const theme = createTheme({
162173
borderRadius: 8, // 8px rounded corners for cards, buttons
163174
},
164175

176+
// zIndex - Consistent layering
177+
zIndex: {
178+
appBar: 1200,
179+
drawer: 1100,
180+
modal: 1300,
181+
snackbar: 1400,
182+
tooltip: 1500,
183+
loadingIndicator: 9999, // Added for loading indicator
184+
},
185+
165186
// Shadows - Consistent elevation
166187
shadows: [
167188
'none',
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
import { render, screen } from '@testing-library/react'
5+
import '@testing-library/jest-dom'
6+
import LoadingIndicator from '@/components/LoadingIndicator'
7+
import { useLoading } from '@/context/LoadingContext'
8+
9+
// Mock the useLoading hook
10+
jest.mock('@/context/LoadingContext', () => ({
11+
useLoading: jest.fn(),
12+
}))
13+
14+
const useLoadingMock = useLoading as jest.Mock
15+
16+
describe('LoadingIndicator', () => {
17+
it('should not render the progressbar when isLoading is false', () => {
18+
useLoadingMock.mockReturnValue({ isLoading: false })
19+
render(<LoadingIndicator />)
20+
// The component is always in the DOM, but its visibility is toggled.
21+
// We check that the progressbar role is not in the document.
22+
expect(screen.queryByRole('progressbar')).toBeNull()
23+
})
24+
25+
it('should be visible when isLoading is true', () => {
26+
useLoadingMock.mockReturnValue({ isLoading: true })
27+
render(<LoadingIndicator />)
28+
expect(screen.getByRole('progressbar')).toBeVisible()
29+
})
30+
})
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
import React from 'react'
5+
import { render, screen, act } from '@testing-library/react'
6+
import '@testing-library/jest-dom'
7+
import { LoadingProvider, useLoading } from '@/context/LoadingContext'
8+
import { Button } from '@mui/material'
9+
10+
const TestComponent = () => {
11+
const { isLoading, setIsLoading } = useLoading()
12+
return (
13+
<div>
14+
<span data-testid="loading-state">
15+
{isLoading ? 'Loading' : 'Not Loading'}
16+
</span>
17+
<Button onClick={() => setIsLoading(true)}>Start Loading</Button>
18+
<Button onClick={() => setIsLoading(false)}>Stop Loading</Button>
19+
</div>
20+
)
21+
}
22+
23+
describe('LoadingProvider', () => {
24+
it('should provide the initial loading state as false', () => {
25+
render(
26+
<LoadingProvider>
27+
<TestComponent />
28+
</LoadingProvider>
29+
)
30+
expect(screen.getByTestId('loading-state')).toHaveTextContent('Not Loading')
31+
})
32+
33+
it('should allow consumers to update the loading state', () => {
34+
render(
35+
<LoadingProvider>
36+
<TestComponent />
37+
</LoadingProvider>
38+
)
39+
40+
act(() => {
41+
screen.getByText('Start Loading').click()
42+
})
43+
44+
expect(screen.getByTestId('loading-state')).toHaveTextContent('Loading')
45+
46+
act(() => {
47+
screen.getByText('Stop Loading').click()
48+
})
49+
50+
expect(screen.getByTestId('loading-state')).toHaveTextContent('Not Loading')
51+
})
52+
})

0 commit comments

Comments
 (0)