diff --git a/app/actions.ts b/app/actions.ts index 878e836..7f5e612 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -15,7 +15,7 @@ export type Option = { export type PrimaryDataType = 'issue' | 'commit' | 'pull-request' | 'snippet' | 'item'; type GridCellState = 'empty' | 'generating' | 'done' | 'error'; -export type ColumnType = 'text' | 'select' | 'select-user' | 'file' | 'issue-pr' | 'commit'; +export type ColumnType = 'text' | 'select' | 'select-user' | 'file' | 'issue-pr' | 'commit' | 'boolean'; export type ColumnResponse = { text: string; @@ -50,7 +50,7 @@ export type ColumnResponse = { message?: string; }>; }; - boolean: boolean; + boolean: { value: boolean }; }; export type GridCell = { diff --git a/app/columns/IssuePRColumnType.tsx b/app/columns/IssuePRColumnType.tsx index bf612ce..efeb0ce 100644 --- a/app/columns/IssuePRColumnType.tsx +++ b/app/columns/IssuePRColumnType.tsx @@ -110,7 +110,7 @@ export const IssuePRColumnType: BaseColumnType<'issue-pr'> = { parsed = JSON.parse(responseContent); } catch (error) { console.error('Failed to parse response content:', error); - return multiple ? { references: [] } : { reference: null }; + return multiple ? { references: [] } : { reference: undefined }; } return multiple ? { references: parsed.references } : { reference: parsed.reference }; }, diff --git a/app/components/Grid.tsx b/app/components/Grid.tsx index 506e98f..821a6aa 100644 --- a/app/components/Grid.tsx +++ b/app/components/Grid.tsx @@ -6,6 +6,7 @@ import { Box } from '@primer/react'; import React, { useEffect } from 'react'; import GridTable from './GridTable'; import Home from './Home'; +import { ThemeToggle } from './ThemeToggle'; import './Grid.css'; @@ -37,6 +38,9 @@ function GridContent({ initialGridId }: { initialGridId?: string }) { backgroundColor: 'canvas.inset', }} > + + + ) : ( diff --git a/app/components/Home.tsx b/app/components/Home.tsx index fb43596..d350187 100644 --- a/app/components/Home.tsx +++ b/app/components/Home.tsx @@ -5,6 +5,7 @@ import { useGridContext } from './GridContext'; import { useRouter } from 'next/navigation'; import NextLink from 'next/link'; import type { Grid } from './GridContext'; +import { ThemeToggle } from './ThemeToggle'; const shuffleArray = (array: string[]) => { for (let i = array.length - 1; i > 0; i--) { @@ -195,6 +196,9 @@ export default function Home() { // backgroundSize: "8px 8px" }} > + + + { + const { theme, toggleTheme } = useTheme(); + + return ( + + ); +}; \ No newline at end of file diff --git a/app/components/__tests__/ThemeProvider.test.tsx b/app/components/__tests__/ThemeProvider.test.tsx new file mode 100644 index 0000000..751e91c --- /dev/null +++ b/app/components/__tests__/ThemeProvider.test.tsx @@ -0,0 +1,77 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { ThemeProvider, useTheme } from '../../utils/theme-context'; +import '@testing-library/jest-dom'; + +// Mock localStorage +const localStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + clear: jest.fn(), +}; + +global.localStorage = localStorageMock; + +// Test component that uses the theme context +const TestComponent = () => { + const { theme, toggleTheme } = useTheme(); + return ( +
+
Theme: {theme}
+ +
+ ); +}; + +describe('ThemeProvider', () => { + beforeEach(() => { + localStorageMock.getItem.mockClear(); + localStorageMock.setItem.mockClear(); + }); + + it('should default to light theme', () => { + localStorageMock.getItem.mockReturnValue(null); + + render( + + + + ); + + expect(screen.getByTestId('theme-display')).toHaveTextContent('Theme: light'); + }); + + it('should toggle theme from light to dark', () => { + localStorageMock.getItem.mockReturnValue(null); + + render( + + + + ); + + expect(screen.getByTestId('theme-display')).toHaveTextContent('Theme: light'); + + fireEvent.click(screen.getByTestId('theme-toggle')); + + expect(screen.getByTestId('theme-display')).toHaveTextContent('Theme: dark'); + }); + + it('should toggle theme from dark to light', () => { + localStorageMock.getItem.mockReturnValue('"dark"'); + + render( + + + + ); + + expect(screen.getByTestId('theme-display')).toHaveTextContent('Theme: dark'); + + fireEvent.click(screen.getByTestId('theme-toggle')); + + expect(screen.getByTestId('theme-display')).toHaveTextContent('Theme: light'); + }); +}); \ No newline at end of file diff --git a/app/functions/updateIssue.ts b/app/functions/updateIssue.ts index 881609c..3fc5715 100644 --- a/app/functions/updateIssue.ts +++ b/app/functions/updateIssue.ts @@ -65,7 +65,6 @@ async function run( stateReason?: 'completed' | 'not_planned' | 'reopened' ) { return await updateIssue({ - repository, title, body, labels, diff --git a/app/grid/[id]/page.tsx b/app/grid/[id]/page.tsx index 9e53d03..9630c8a 100644 --- a/app/grid/[id]/page.tsx +++ b/app/grid/[id]/page.tsx @@ -1,17 +1,12 @@ import { createPrimaryColumn, hydrateCell } from '../../actions'; -import { BaseStyles, ThemeProvider } from '@primer/react'; import Grid from '../../components/Grid'; export default function GridPage({ params }: { params: { id: string } }) { return ( - - - - - + ); } diff --git a/app/layout.tsx b/app/layout.tsx index 6ceb217..d722192 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,4 +1,6 @@ import type { Metadata } from 'next'; +import { ThemeProvider } from './utils/theme-context'; +import { BaseStyles } from '@primer/react'; export const metadata: Metadata = { title: '🕵🏻‍♂️ Grid Agent', @@ -12,7 +14,13 @@ export default function RootLayout({ }>) { return ( - {children} + + + + {children} + + + ); } diff --git a/app/page.tsx b/app/page.tsx index f011b20..0189ebb 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,14 +1,8 @@ import { createPrimaryColumn, hydrateCell } from './actions'; -import { BaseStyles, ThemeProvider } from '@primer/react'; - import Grid from './components/Grid'; export default function Page() { return ( - - - - - + ); } diff --git a/app/utils/theme-context.tsx b/app/utils/theme-context.tsx new file mode 100644 index 0000000..0cbee3a --- /dev/null +++ b/app/utils/theme-context.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { createContext, useContext, ReactNode } from 'react'; +import { ThemeProvider as PrimerThemeProvider } from '@primer/react'; +import useLocalStorage from './local-storage'; + +type ThemeMode = 'light' | 'dark'; + +interface ThemeContextType { + theme: ThemeMode; + toggleTheme: () => void; +} + +const ThemeContext = createContext(undefined); + +export const useTheme = () => { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; + +interface ThemeProviderProps { + children: ReactNode; +} + +export const ThemeProvider = ({ children }: ThemeProviderProps) => { + const [theme, setTheme] = useLocalStorage('theme', 'light'); + + const toggleTheme = () => { + setTheme(theme === 'light' ? 'dark' : 'light'); + }; + + return ( + + + {children} + + + ); +}; \ No newline at end of file