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
2 changes: 2 additions & 0 deletions app/components/GridHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { TextInput, Text, ActionMenu, ActionList, Box, Button, CounterLabel } fr
import { ArrowLeftIcon } from '@primer/octicons-react';
import { SearchIcon } from '@primer/octicons-react';
import { useGridContext } from './GridContext';
import { ThemeSwitcher } from './ThemeSwitcher';
import NextLink from 'next/link';

export function Search() {
Expand Down Expand Up @@ -167,6 +168,7 @@ export function GridHeader({ title, setShowNewColumnForm, count }: GridHeaderPro
<Search />
<GroupBy />
<FilterBy />
<ThemeSwitcher />
<Button onClick={handleSaveGist} disabled={isSavingGist}>
Save to gist
</Button>
Expand Down
59 changes: 59 additions & 0 deletions app/components/ThemeContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use client';

import React, { createContext, useContext, useEffect, useState } from 'react';

type ColorMode = 'light' | 'dark';

interface ThemeContextType {
colorMode: ColorMode;
setColorMode: (mode: ColorMode) => void;
toggleColorMode: () => void;
isLoaded: boolean;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};

interface ThemeProviderProps {
children: React.ReactNode;
}

export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
const [colorMode, setColorModeState] = useState<ColorMode>('light');
const [isLoaded, setIsLoaded] = useState(false);

// Load saved theme preference on mount
useEffect(() => {
const savedTheme = localStorage.getItem('colorMode') as ColorMode;
if (savedTheme && (savedTheme === 'light' || savedTheme === 'dark')) {
setColorModeState(savedTheme);
}
setIsLoaded(true);
}, []);

const setColorMode = (mode: ColorMode) => {
setColorModeState(mode);
localStorage.setItem('colorMode', mode);
};

const toggleColorMode = () => {
const newMode = colorMode === 'light' ? 'dark' : 'light';
setColorMode(newMode);
};

const value = {
colorMode,
setColorMode,
toggleColorMode,
isLoaded,
};

return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
};
19 changes: 19 additions & 0 deletions app/components/ThemeSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client';

import React from 'react';
import { IconButton } from '@primer/react';
import { SunIcon, MoonIcon } from '@primer/octicons-react';
import { useTheme } from './ThemeContext';

export const ThemeSwitcher: React.FC = () => {
const { colorMode, toggleColorMode } = useTheme();

return (
<IconButton
aria-label={`Switch to ${colorMode === 'light' ? 'dark' : 'light'} mode`}
icon={colorMode === 'light' ? MoonIcon : SunIcon}
variant="invisible"
onClick={toggleColorMode}
/>
);
};
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
37 changes: 33 additions & 4 deletions app/grid/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,46 @@
'use client';

import { createPrimaryColumn, hydrateCell } from '../../actions';
import { BaseStyles, ThemeProvider } from '@primer/react';
import { BaseStyles, ThemeProvider as PrimerThemeProvider } from '@primer/react';
import { ThemeProvider } from '../../components/ThemeContext';
import { useTheme } from '../../components/ThemeContext';
import Grid from '../../components/Grid';

export default function GridPage({ params }: { params: { id: string } }) {
function ThemedApp({ initialGridId }: { initialGridId: string }) {
const { colorMode, isLoaded } = useTheme();

// Don't render until theme is loaded to prevent hydration mismatch
if (!isLoaded) {
return (
<PrimerThemeProvider colorMode="light">
<BaseStyles>
<Grid
createPrimaryColumn={createPrimaryColumn}
hydrateCell={hydrateCell}
initialGridId={initialGridId}
/>
</BaseStyles>
</PrimerThemeProvider>
);
}

return (
<ThemeProvider>
<PrimerThemeProvider colorMode={colorMode}>
<BaseStyles>
<Grid
createPrimaryColumn={createPrimaryColumn}
hydrateCell={hydrateCell}
initialGridId={params.id}
initialGridId={initialGridId}
/>
</BaseStyles>
</PrimerThemeProvider>
);
}

export default function GridPage({ params }: { params: { id: string } }) {
return (
<ThemeProvider>
<ThemedApp initialGridId={params.id} />
</ThemeProvider>
);
}
31 changes: 28 additions & 3 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
'use client';

import { createPrimaryColumn, hydrateCell } from './actions';
import { BaseStyles, ThemeProvider } from '@primer/react';
import { BaseStyles, ThemeProvider as PrimerThemeProvider } from '@primer/react';
import { ThemeProvider } from './components/ThemeContext';
import { useTheme } from './components/ThemeContext';

import Grid from './components/Grid';

export default function Page() {
function ThemedApp() {
const { colorMode, isLoaded } = useTheme();

// Don't render until theme is loaded to prevent hydration mismatch
if (!isLoaded) {
return (
<PrimerThemeProvider colorMode="light">
<BaseStyles>
<Grid createPrimaryColumn={createPrimaryColumn} hydrateCell={hydrateCell} />
</BaseStyles>
</PrimerThemeProvider>
);
}

return (
<ThemeProvider>
<PrimerThemeProvider colorMode={colorMode}>
<BaseStyles>
<Grid createPrimaryColumn={createPrimaryColumn} hydrateCell={hydrateCell} />
</BaseStyles>
</PrimerThemeProvider>
);
}

export default function Page() {
return (
<ThemeProvider>
<ThemedApp />
</ThemeProvider>
);
}