Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,7 +50,7 @@ export type ColumnResponse = {
message?: string;
}>;
};
boolean: boolean;
boolean: { value: boolean };
};

export type GridCell<T extends keyof ColumnResponse = ColumnType> = {
Expand Down
2 changes: 1 addition & 1 deletion app/columns/IssuePRColumnType.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
},
Expand Down
4 changes: 4 additions & 0 deletions app/components/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -37,6 +38,9 @@ function GridContent({ initialGridId }: { initialGridId?: string }) {
backgroundColor: 'canvas.inset',
}}
>
<Box sx={{ position: 'absolute', top: 2, right: 2, zIndex: 1 }}>
<ThemeToggle />
</Box>
<GridTable />
</Box>
) : (
Expand Down
4 changes: 4 additions & 0 deletions app/components/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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--) {
Expand Down Expand Up @@ -195,6 +196,9 @@ export default function Home() {
// backgroundSize: "8px 8px"
}}
>
<Box sx={{ position: 'absolute', top: 2, right: 2 }}>
<ThemeToggle />
</Box>
<Box sx={{ width: '740' }}>
<Box
as="h1"
Expand Down
18 changes: 18 additions & 0 deletions app/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client';

import { IconButton } from '@primer/react';
import { SunIcon, MoonIcon } from '@primer/octicons-react';
import { useTheme } from '../utils/theme-context';

export const ThemeToggle = () => {
const { theme, toggleTheme } = useTheme();

return (
<IconButton
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
icon={theme === 'light' ? MoonIcon : SunIcon}
onClick={toggleTheme}
variant="invisible"
/>
);
};
77 changes: 77 additions & 0 deletions app/components/__tests__/ThemeProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<div data-testid="theme-display">Theme: {theme}</div>
<button onClick={toggleTheme} data-testid="theme-toggle">
Toggle Theme
</button>
</div>
);
};

describe('ThemeProvider', () => {
beforeEach(() => {
localStorageMock.getItem.mockClear();
localStorageMock.setItem.mockClear();
});

it('should default to light theme', () => {
localStorageMock.getItem.mockReturnValue(null);

render(
<ThemeProvider>
<TestComponent />
</ThemeProvider>
);

expect(screen.getByTestId('theme-display')).toHaveTextContent('Theme: light');
});

it('should toggle theme from light to dark', () => {
localStorageMock.getItem.mockReturnValue(null);

render(
<ThemeProvider>
<TestComponent />
</ThemeProvider>
);

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(
<ThemeProvider>
<TestComponent />
</ThemeProvider>
);

expect(screen.getByTestId('theme-display')).toHaveTextContent('Theme: dark');

fireEvent.click(screen.getByTestId('theme-toggle'));

expect(screen.getByTestId('theme-display')).toHaveTextContent('Theme: light');
});
});
1 change: 0 additions & 1 deletion app/functions/updateIssue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ async function run(
stateReason?: 'completed' | 'not_planned' | 'reopened'
) {
return await updateIssue({
repository,
title,
body,
labels,
Expand Down
15 changes: 5 additions & 10 deletions app/grid/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ThemeProvider>
<BaseStyles>
<Grid
createPrimaryColumn={createPrimaryColumn}
hydrateCell={hydrateCell}
initialGridId={params.id}
/>
</BaseStyles>
</ThemeProvider>
<Grid
createPrimaryColumn={createPrimaryColumn}
hydrateCell={hydrateCell}
initialGridId={params.id}
/>
);
}
10 changes: 9 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -12,7 +14,13 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body>{children}</body>
<body>
<ThemeProvider>
<BaseStyles>
{children}
</BaseStyles>
</ThemeProvider>
</body>
</html>
);
}
8 changes: 1 addition & 7 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ThemeProvider>
<BaseStyles>
<Grid createPrimaryColumn={createPrimaryColumn} hydrateCell={hydrateCell} />
</BaseStyles>
</ThemeProvider>
<Grid createPrimaryColumn={createPrimaryColumn} hydrateCell={hydrateCell} />
);
}
42 changes: 42 additions & 0 deletions app/utils/theme-context.tsx
Original file line number Diff line number Diff line change
@@ -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<ThemeContextType | undefined>(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<ThemeMode>('theme', 'light');

const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};

return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<PrimerThemeProvider colorMode={theme}>
{children}
</PrimerThemeProvider>
</ThemeContext.Provider>
);
};