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
213 changes: 92 additions & 121 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,140 +1,111 @@
import React, { useState, useEffect } from 'react';
import { GitHubUser, Repository, Issue, AppRoute } from './types';
import { onMount, onCleanup, Show, createEffect } from 'solid-js';
import { AppRoute } from './types';
import { TokenGate } from './views/TokenGate';
import { Dashboard } from './views/Dashboard';
import { RepoDetail } from './views/RepoDetail';
import { IssueDetail } from './views/IssueDetail';
import { signOutFromFirebase, handleRedirectResult } from './services/firebaseService';
import { validateToken } from './services/githubService';
import { ThemeProvider } from './contexts/ThemeContext';

const App: React.FC = () => {
const [token, setToken] = useState<string | null>(localStorage.getItem('gh_token'));
const [user, setUser] = useState<GitHubUser | null>(
localStorage.getItem('gh_user') ? JSON.parse(localStorage.getItem('gh_user')!) : null
);
const [checkingRedirect, setCheckingRedirect] = useState(true);

const [currentRoute, setCurrentRoute] = useState<AppRoute>(
token && user ? AppRoute.REPO_LIST : AppRoute.TOKEN_INPUT
);
const [selectedRepo, setSelectedRepo] = useState<Repository | null>(null);
const [selectedIssue, setSelectedIssue] = useState<Issue | null>(null);

import {
store,
handleLogin,
handleLogout,
navigateToRepo,
navigateBack,
navigateToIssue,
navigateBackToRepo,
updateResolvedTheme
} from './store';

const App = () => {
// Handle redirect result from Firebase OAuth (for popup-blocked fallback)
useEffect(() => {
const checkRedirectResult = async () => {
try {
const result = await handleRedirectResult();
if (result) {
// Validate token and get user data from GitHub API
const ghUser = await validateToken(result.accessToken);
handleLogin(result.accessToken, ghUser);
}
} catch (err) {
console.error('Redirect result error:', err);
} finally {
setCheckingRedirect(false);
}
};

checkRedirectResult();
}, []);

const handleLogin = (newToken: string, newUser: GitHubUser) => {
setToken(newToken);
setUser(newUser);
localStorage.setItem('gh_token', newToken);
localStorage.setItem('gh_user', JSON.stringify(newUser));
setCurrentRoute(AppRoute.REPO_LIST);
};

const handleLogout = async () => {
// Sign out from Firebase
onMount(async () => {
try {
await signOutFromFirebase();
const result = await handleRedirectResult();
if (result) {
// Validate token and get user data from GitHub API
const ghUser = await validateToken(result.accessToken);
handleLogin(result.accessToken, ghUser);
}
} catch (err) {
console.error('Firebase sign out error:', err);
console.error('Redirect result error:', err);
} finally {
store.checkingRedirect = false;
}

setToken(null);
setUser(null);
localStorage.removeItem('gh_token');
localStorage.removeItem('gh_user');
setCurrentRoute(AppRoute.TOKEN_INPUT);
setSelectedRepo(null);
};

const navigateToRepo = (repo: Repository) => {
setSelectedRepo(repo);
setCurrentRoute(AppRoute.REPO_DETAIL);
};
});

// Theme effect
createEffect(() => {
updateResolvedTheme();
});

// Listen for OS preference changes
onMount(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handler = () => {
if (store.theme === 'system') {
updateResolvedTheme();
}
};

const navigateBack = () => {
setSelectedRepo(null);
setSelectedIssue(null);
setCurrentRoute(AppRoute.REPO_LIST);
};
mediaQuery.addEventListener('change', handler);
onCleanup(() => mediaQuery.removeEventListener('change', handler));
});

const navigateToIssue = (issue: Issue) => {
setSelectedIssue(issue);
setCurrentRoute(AppRoute.ISSUE_DETAIL);
};
// Update DOM class based on theme
createEffect(() => {
if (store.resolvedTheme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
});

const navigateBackToRepo = () => {
setSelectedIssue(null);
setCurrentRoute(AppRoute.REPO_DETAIL);
};
const onLogout = () => handleLogout(signOutFromFirebase);

// Render Logic
if (checkingRedirect) {
return (
<div className="min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-900">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-slate-900 dark:border-slate-100"></div>
</div>
);
}

if (currentRoute === AppRoute.TOKEN_INPUT || !token || !user) {
return <TokenGate onSuccess={handleLogin} />;
}

if (currentRoute === AppRoute.ISSUE_DETAIL && selectedRepo && selectedIssue) {
return (
<IssueDetail
token={token}
repo={selectedRepo}
issue={selectedIssue}
onBack={navigateBackToRepo}
/>
);
}

if (currentRoute === AppRoute.REPO_DETAIL && selectedRepo) {
return (
<RepoDetail
token={token}
repo={selectedRepo}
onBack={navigateBack}
onIssueSelect={navigateToIssue}
/>
);
}

return (
<Dashboard
token={token}
user={user}
onRepoSelect={navigateToRepo}
onLogout={handleLogout}
/>
<Show
when={!store.checkingRedirect}
fallback={
<div class="min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-900">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-slate-900 dark:border-slate-100"></div>
</div>
}
>
<Show
when={store.currentRoute !== AppRoute.TOKEN_INPUT && store.token && store.user}
fallback={<TokenGate onSuccess={handleLogin} />}
>
<Show when={store.currentRoute === AppRoute.ISSUE_DETAIL && store.selectedRepo && store.selectedIssue}>
<IssueDetail
token={store.token!}
repo={store.selectedRepo!}
issue={store.selectedIssue!}
onBack={navigateBackToRepo}
/>
</Show>

<Show when={store.currentRoute === AppRoute.REPO_DETAIL && store.selectedRepo}>
<RepoDetail
token={store.token!}
repo={store.selectedRepo!}
onBack={navigateBack}
onIssueSelect={navigateToIssue}
/>
</Show>

<Show when={store.currentRoute === AppRoute.REPO_LIST}>
<Dashboard
token={store.token!}
user={store.user!}
onRepoSelect={navigateToRepo}
onLogout={onLogout}
/>
</Show>
</Show>
</Show>
);
};

const AppWithProviders: React.FC = () => (
<ThemeProvider>
<App />
</ThemeProvider>
);

export default AppWithProviders;
export default App;
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,34 @@ Agents that work with this pattern:

## Tech Stack

- **React 18** + TypeScript
- **Vite** for fast builds
- **SolidJS** + TypeScript (migrated from React 19)
- **Vite** with `vite-plugin-solid` for fast builds
- **Tailwind CSS** for styling
- **Firebase Auth** for GitHub OAuth
- **GitHub REST API** for repository operations
- **State Management**: `createMutable` from solid-js/store (no external state libraries)

## License

MIT

## Recent Updates

### SolidJS Migration ✨

This project has been migrated from React 19 to **SolidJS** with exclusive use of `createMutable` for state management.

**Benefits:**
- ⚡ Faster reactivity with fine-grained updates
- 🎯 No virtual DOM overhead
- 📦 Smaller bundle size
- 🔧 Simpler state management with `createMutable`

**Migration Status:** Core functionality complete. See [SOLIDJS_MIGRATION_STATUS.md](./SOLIDJS_MIGRATION_STATUS.md) for details.

**Key Changes:**
- All components converted to SolidJS
- State management via `createMutable` (no signals, no external libraries)
- Dashboard fully functional with all CRUD operations
- Build process updated for SolidJS

Loading