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
125 changes: 125 additions & 0 deletions App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { onMount } from 'solid-js';
import { createMutable } from 'solid-js/store';
import { AppRoute } from './types.js';
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';

function App() {
const state = createMutable({
token: localStorage.getItem('gh_token'),
user: localStorage.getItem('gh_user') ? JSON.parse(localStorage.getItem('gh_user')) : null,
checkingRedirect: true,
currentRoute: localStorage.getItem('gh_token') && localStorage.getItem('gh_user') ? AppRoute.REPO_LIST : AppRoute.TOKEN_INPUT,
selectedRepo: null,
selectedIssue: null,
});

onMount(async () => {
try {
const result = await handleRedirectResult();
if (result) {
const ghUser = await validateToken(result.accessToken);
handleLogin(result.accessToken, ghUser);
}
} catch (err) {
console.error('Redirect result error:', err);
} finally {
state.checkingRedirect = false;
}
});

const handleLogin = (newToken, newUser) => {
state.token = newToken;
state.user = newUser;
localStorage.setItem('gh_token', newToken);
localStorage.setItem('gh_user', JSON.stringify(newUser));
state.currentRoute = AppRoute.REPO_LIST;
};

const handleLogout = async () => {
try {
await signOutFromFirebase();
} catch (err) {
console.error('Firebase sign out error:', err);
}

state.token = null;
state.user = null;
localStorage.removeItem('gh_token');
localStorage.removeItem('gh_user');
state.currentRoute = AppRoute.TOKEN_INPUT;
state.selectedRepo = null;
};

const navigateToRepo = (repo) => {
state.selectedRepo = repo;
state.currentRoute = AppRoute.REPO_DETAIL;
};

const navigateBack = () => {
state.selectedRepo = null;
state.selectedIssue = null;
state.currentRoute = AppRoute.REPO_LIST;
};

const navigateToIssue = (issue) => {
state.selectedIssue = issue;
state.currentRoute = AppRoute.ISSUE_DETAIL;
};

const navigateBackToRepo = () => {
state.selectedIssue = null;
state.currentRoute = AppRoute.REPO_DETAIL;
};

return (
<Show when={!state.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>
}>
<Switch>
<Match when={state.currentRoute === AppRoute.TOKEN_INPUT || !state.token || !state.user}>
<TokenGate onSuccess={handleLogin} />
</Match>
<Match when={state.currentRoute === AppRoute.ISSUE_DETAIL && state.selectedRepo && state.selectedIssue}>
<IssueDetail
token={state.token}
repo={state.selectedRepo}
issue={state.selectedIssue}
onBack={navigateBackToRepo}
/>
</Match>
<Match when={state.currentRoute === AppRoute.REPO_DETAIL && state.selectedRepo}>
<RepoDetail
token={state.token}
repo={state.selectedRepo}
onBack={navigateBack}
onIssueSelect={navigateToIssue}
/>
</Match>
<Match when={state.currentRoute === AppRoute.REPO_LIST}>
<Dashboard
token={state.token}
user={state.user}
onRepoSelect={navigateToRepo}
onLogout={handleLogout}
/>
</Match>
</Switch>
</Show>
);
};

const AppWithProviders = () => (
<ThemeProvider>
<App />
</ThemeProvider>
);

export default AppWithProviders;
140 changes: 0 additions & 140 deletions App.tsx

This file was deleted.

50 changes: 50 additions & 0 deletions components/Button.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { splitProps } from 'solid-js';

export const Button = (props) => {
const [local, others] = splitProps(props, [
'children',
'variant',
'size',
'class',
'isLoading',
'icon',
]);

const variant = () => local.variant || 'primary';
const size = () => local.size || 'md';

const baseStyles = "inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-slate-900 disabled:opacity-50 disabled:cursor-not-allowed";

const sizeStyles = {
sm: "px-2.5 py-1.5 text-xs",
md: "px-4 py-2 text-sm",
};

const variants = {
primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
secondary: "bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 border border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700 focus:ring-blue-500",
danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
ghost: "bg-transparent text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 focus:ring-slate-500",
magic: "bg-gradient-to-r from-purple-500 to-indigo-600 text-white hover:from-purple-600 hover:to-indigo-700 focus:ring-purple-500",
};

return (
<button
class={`${baseStyles} ${sizeStyles[size()]} ${variants[variant()]} ${local.class || ''}`}
disabled={local.isLoading || others.disabled}
{...others}
>
<Show when={local.isLoading} fallback={
<Show when={local.icon}>
<span class="mr-2">{local.icon}</span>
</Show>
}>
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-current" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</Show>
{local.children}
</button>
);
};
51 changes: 0 additions & 51 deletions components/Button.tsx

This file was deleted.

Loading