Skip to content
Open
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
99 changes: 51 additions & 48 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { createEffect, onMount } from 'solid-js';
import { createMutable } from 'solid-js/store';
import { GitHubUser, Repository, Issue, AppRoute } from './types';
import { TokenGate } from './views/TokenGate';
import { Dashboard } from './views/Dashboard';
Expand All @@ -8,45 +9,47 @@ import { signOutFromFirebase, handleRedirectResult } from './services/firebaseSe
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);
const App = () => {
const state = createMutable({
token: localStorage.getItem('gh_token'),
user: localStorage.getItem('gh_user') ? JSON.parse(localStorage.getItem('gh_user')!) : null,
checkingRedirect: true,
currentRoute: AppRoute.TOKEN_INPUT as AppRoute,
selectedRepo: null as Repository | null,
selectedIssue: null as Issue | null,
});

// Set initial route based on token and user
createEffect(() => {
if (state.token && state.user) {
state.currentRoute = AppRoute.REPO_LIST;
} else {
state.currentRoute = AppRoute.TOKEN_INPUT;
}
});

// 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);
onMount(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);
}
};

checkRedirectResult();
}, []);
} catch (err) {
console.error('Redirect result error:', err);
} finally {
state.checkingRedirect = false;
}
});

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

const handleLogout = async () => {
Expand All @@ -56,34 +59,34 @@ const App: React.FC = () => {
} catch (err) {
console.error('Firebase sign out error:', err);
}
setToken(null);
setUser(null);

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

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

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

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

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

// Render Logic
Expand Down Expand Up @@ -131,7 +134,7 @@ const App: React.FC = () => {
);
};

const AppWithProviders: React.FC = () => (
const AppWithProviders = () => (
<ThemeProvider>
<App />
</ThemeProvider>
Expand Down
31 changes: 15 additions & 16 deletions components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import React from 'react';

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'magic';
size?: 'sm' | 'md';
isLoading?: boolean;
icon?: React.ReactNode;
icon?: any;
}

export const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'md',
className = '',
isLoading = false,
icon,
...props
}) => {
export const Button = (props: ButtonProps) => {
const {
children,
variant = 'primary',
size = 'md',
className = '',
isLoading = false,
icon,
...rest
} = props;
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 = {
Expand All @@ -32,10 +31,10 @@ export const Button: React.FC<ButtonProps> = ({
};

return (
<button
<button
className={`${baseStyles} ${sizeStyles[size]} ${variants[variant]} ${className}`}
disabled={isLoading || props.disabled}
{...props}
disabled={isLoading || rest.disabled}
{...rest}
>
{isLoading ? (
<svg className="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">
Expand Down
19 changes: 9 additions & 10 deletions components/Markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import ReactMarkdown from 'react-markdown';
import { SolidMarkdown } from 'solid-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';

Expand All @@ -8,7 +7,7 @@ interface MarkdownProps {
className?: string;
}

export const Markdown: React.FC<MarkdownProps> = ({ children, className = '' }) => {
export const Markdown = (props: MarkdownProps) => {
return (
<div className={`prose prose-sm max-w-none prose-slate dark:prose-invert
prose-headings:text-slate-800 dark:prose-headings:text-slate-100 prose-headings:font-semibold
Expand All @@ -30,8 +29,8 @@ export const Markdown: React.FC<MarkdownProps> = ({ children, className = '' })
[&_summary]:before:content-['▶'] [&_summary]:before:inline-block [&_summary]:before:mr-2 [&_summary]:before:text-xs [&_summary]:before:transition-transform
[&_details[open]>summary]:before:content-['▼']
[&_details[open]>summary]:mb-2
${className}`}>
<ReactMarkdown
${props.className || ''}`}>
<SolidMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
components={{
Expand All @@ -43,17 +42,17 @@ export const Markdown: React.FC<MarkdownProps> = ({ children, className = '' })
),
// Better image handling
img: ({ src, alt }) => (
<img
src={src}
alt={alt || ''}
<img
src={src}
alt={alt || ''}
loading="lazy"
className="rounded-lg max-w-full h-auto"
/>
),
}}
>
{children}
</ReactMarkdown>
{props.children}
</SolidMarkdown>
</div>
);
};
54 changes: 26 additions & 28 deletions components/RepoCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react';
import { Repository, Issue } from '../types';
import { Star, Lock, Globe, Trash2, Pin, CircleDot } from 'lucide-react';

Expand All @@ -11,42 +10,42 @@ interface RepoCardProps {
issues?: Issue[];
}

export const RepoCard: React.FC<RepoCardProps> = ({ repo, onClick, onDelete, onPin, isPinned, issues }) => {
const handleDeleteClick = (e: React.MouseEvent) => {
export const RepoCard = (props: RepoCardProps) => {
const handleDeleteClick = (e: MouseEvent) => {
e.stopPropagation();
onDelete?.(repo);
props.onDelete?.(props.repo);
};

const handlePinClick = (e: React.MouseEvent) => {
const handlePinClick = (e: MouseEvent) => {
e.stopPropagation();
onPin?.(repo);
props.onPin?.(props.repo);
};

return (
<div
onClick={() => onClick(repo)}
<div
onClick={() => props.onClick(props.repo)}
className="bg-white dark:bg-slate-800 p-5 rounded-lg shadow-sm border border-slate-200 dark:border-slate-700 hover:shadow-md dark:hover:shadow-slate-900/50 transition-shadow cursor-pointer flex flex-col h-full group"
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2 min-w-0 flex-1">
{repo.private ? <Lock size={16} className="text-slate-400 dark:text-slate-500 flex-shrink-0" /> : <Globe size={16} className="text-slate-400 dark:text-slate-500 flex-shrink-0" />}
<h3 className="text-lg font-semibold text-slate-800 dark:text-slate-100 break-all">{repo.name}</h3>
{props.repo.private ? <Lock size={16} className="text-slate-400 dark:text-slate-500 flex-shrink-0" /> : <Globe size={16} className="text-slate-400 dark:text-slate-500 flex-shrink-0" />}
<h3 className="text-lg font-semibold text-slate-800 dark:text-slate-100 break-all">{props.repo.name}</h3>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
{onPin && (
{props.onPin && (
<button
onClick={handlePinClick}
className={`p-1.5 rounded-md transition-all ${
isPinned
? 'text-blue-500 bg-blue-50 dark:bg-blue-900/30'
props.isPinned
? 'text-blue-500 bg-blue-50 dark:bg-blue-900/30'
: 'text-slate-400 dark:text-slate-500 hover:text-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/30 opacity-0 group-hover:opacity-100'
}`}
title={isPinned ? "Unpin repository" : "Pin repository"}
title={props.isPinned ? "Unpin repository" : "Pin repository"}
>
<Pin size={16} />
</button>
)}
{onDelete && (
{props.onDelete && (
<button
onClick={handleDeleteClick}
className="p-1.5 text-slate-400 dark:text-slate-500 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-md opacity-0 group-hover:opacity-100 transition-all"
Expand All @@ -56,29 +55,28 @@ export const RepoCard: React.FC<RepoCardProps> = ({ repo, onClick, onDelete, onP
</button>
)}
<span className="text-xs px-2 py-1 bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 rounded-full border border-slate-200 dark:border-slate-600">
{repo.language || 'Text'}
{props.repo.language || 'Text'}
</span>
</div>
</div>

<p className="text-slate-500 dark:text-slate-400 text-sm mb-3 line-clamp-2">
{repo.description || "No description provided."}
{props.repo.description || "No description provided."}
</p>

{issues && issues.length > 0 && (
{props.issues && props.issues.length > 0 && (
<div className="mb-3 space-y-1.5">
{issues.map((issue) => (
<div
key={issue.id}
{props.issues.map((issue) => (
<div
className="flex items-start gap-2 text-xs"
onClick={(e) => {
e.stopPropagation();
window.open(issue.html_url, '_blank');
}}
>
<CircleDot
size={12}
className={`mt-0.5 flex-shrink-0 ${issue.state === 'open' ? 'text-green-500' : 'text-purple-500'}`}
<CircleDot
size={12}
className={`mt-0.5 flex-shrink-0 ${issue.state === 'open' ? 'text-green-500' : 'text-purple-500'}`}
/>
<span className="text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 hover:underline truncate cursor-pointer">
{issue.title}
Expand All @@ -91,14 +89,14 @@ export const RepoCard: React.FC<RepoCardProps> = ({ repo, onClick, onDelete, onP
<div className="flex items-center gap-4 text-slate-500 dark:text-slate-400 text-sm mt-auto pt-3 border-t border-slate-100 dark:border-slate-700">
<div className="flex items-center gap-1">
<Star size={14} />
<span>{repo.stargazers_count}</span>
<span>{props.repo.stargazers_count}</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 rounded-full bg-green-500" />
<span>{new Date(repo.updated_at).toLocaleDateString()}</span>
<span>{new Date(props.repo.updated_at).toLocaleDateString()}</span>
</div>
<div className="ml-auto text-xs text-slate-400 dark:text-slate-500">
{repo.open_issues_count} issues
{props.repo.open_issues_count} issues
</div>
</div>
</div>
Expand Down
3 changes: 1 addition & 2 deletions components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React from 'react';
import { Sun, Moon, Monitor } from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';

export const ThemeToggle: React.FC = () => {
export const ThemeToggle = () => {
const { theme, setTheme } = useTheme();

const cycleTheme = () => {
Expand Down
Loading