From 0ac662a5503106ce3f32c700a9cfc54b8b504374 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 05:54:59 +0000 Subject: [PATCH 01/11] Initial plan From 30dcf7c53af592f34fe0e5838a9ba7f3dc9b74d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 06:01:05 +0000 Subject: [PATCH 02/11] Convert core files and components to SolidJS with createMutable Co-authored-by: friuns <7095563+friuns@users.noreply.github.com> --- App.tsx | 213 ++++++++++++++++--------------------- components/Button.tsx | 42 ++++---- components/Markdown.tsx | 38 ++----- components/RepoCard.tsx | 113 ++++++++++---------- components/ThemeToggle.tsx | 27 ++--- components/Toast.tsx | 65 ++++++----- index.tsx | 11 +- package.json | 12 +-- store.ts | 117 ++++++++++++++++++++ tsconfig.json | 3 +- vite.config.ts | 4 +- 11 files changed, 351 insertions(+), 294 deletions(-) create mode 100644 store.ts diff --git a/App.tsx b/App.tsx index 0d43b03..11e9d0a 100644 --- a/App.tsx +++ b/App.tsx @@ -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(localStorage.getItem('gh_token')); - const [user, setUser] = useState( - localStorage.getItem('gh_user') ? JSON.parse(localStorage.getItem('gh_user')!) : null - ); - const [checkingRedirect, setCheckingRedirect] = useState(true); - - const [currentRoute, setCurrentRoute] = useState( - token && user ? AppRoute.REPO_LIST : AppRoute.TOKEN_INPUT - ); - const [selectedRepo, setSelectedRepo] = useState(null); - const [selectedIssue, setSelectedIssue] = useState(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 ( -
-
-
- ); - } - - if (currentRoute === AppRoute.TOKEN_INPUT || !token || !user) { - return ; - } - - if (currentRoute === AppRoute.ISSUE_DETAIL && selectedRepo && selectedIssue) { - return ( - - ); - } - - if (currentRoute === AppRoute.REPO_DETAIL && selectedRepo) { - return ( - - ); - } - return ( - + +
+ + } + > + } + > + + + + + + + + + + + + +
); }; -const AppWithProviders: React.FC = () => ( - - - -); - -export default AppWithProviders; +export default App; diff --git a/components/Button.tsx b/components/Button.tsx index bfde209..d09f411 100644 --- a/components/Button.tsx +++ b/components/Button.tsx @@ -1,21 +1,16 @@ -import React from 'react'; +import { Component, JSX, Show, mergeProps, splitProps } from 'solid-js'; -interface ButtonProps extends React.ButtonHTMLAttributes { +interface ButtonProps extends JSX.ButtonHTMLAttributes { variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'magic'; size?: 'sm' | 'md'; isLoading?: boolean; - icon?: React.ReactNode; + icon?: JSX.Element; } -export const Button: React.FC = ({ - children, - variant = 'primary', - size = 'md', - className = '', - isLoading = false, - icon, - ...props -}) => { +export const Button: Component = (props) => { + const merged = mergeProps({ variant: 'primary', size: 'md', isLoading: false, class: '' }, props); + const [local, buttonProps] = splitProps(merged, ['children', 'variant', 'size', 'class', 'isLoading', 'icon', 'disabled']); + 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 = { @@ -33,19 +28,20 @@ export const Button: React.FC = ({ return ( ); }; diff --git a/components/Markdown.tsx b/components/Markdown.tsx index f950a20..f25382a 100644 --- a/components/Markdown.tsx +++ b/components/Markdown.tsx @@ -1,16 +1,14 @@ -import React from 'react'; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; -import rehypeRaw from 'rehype-raw'; +import { Component } from 'solid-js'; +import SolidMarkdown from 'solid-markdown'; interface MarkdownProps { children: string; - className?: string; + class?: string; } -export const Markdown: React.FC = ({ children, className = '' }) => { +export const Markdown: Component = (props) => { return ( -
= ({ 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}`}> - ( - - {children} - - ), - // Better image handling - img: ({ src, alt }) => ( - {alt - ), - }} - > - {children} - + ${props.class || ''}`}> + {props.children}
); }; diff --git a/components/RepoCard.tsx b/components/RepoCard.tsx index 305a834..1249b09 100644 --- a/components/RepoCard.tsx +++ b/components/RepoCard.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import { Component, For, Show, JSX } from 'solid-js'; import { Repository, Issue } from '../types'; -import { Star, Lock, Globe, Trash2, Pin, CircleDot } from 'lucide-react'; +import { Star, Lock, Globe, Trash2, Pin, CircleDot } from 'lucide-solid'; interface RepoCardProps { repo: Repository; @@ -11,94 +11,97 @@ interface RepoCardProps { issues?: Issue[]; } -export const RepoCard: React.FC = ({ repo, onClick, onDelete, onPin, isPinned, issues }) => { - const handleDeleteClick = (e: React.MouseEvent) => { +export const RepoCard: Component = (props) => { + 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 (
onClick(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" + onClick={() => props.onClick(props.repo)} + class="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" > -
-
- {repo.private ? : } -

{repo.name}

+
+
+ }> + + +

{props.repo.name}

-
- {onPin && ( +
+ - )} - {onDelete && ( + + - )} - - {repo.language || 'Text'} + + + {props.repo.language || 'Text'}
-

- {repo.description || "No description provided."} +

+ {props.repo.description || "No description provided."}

- {issues && issues.length > 0 && ( -
- {issues.map((issue) => ( -
{ - e.stopPropagation(); - window.open(issue.html_url, '_blank'); - }} - > - - - {issue.title} - -
- ))} + 0}> +
+ + {(issue) => ( +
{ + e.stopPropagation(); + window.open(issue.html_url, '_blank'); + }} + > + + + {issue.title} + +
+ )} +
- )} +
-
-
+
+
- {repo.stargazers_count} + {props.repo.stargazers_count}
-
-
- {new Date(repo.updated_at).toLocaleDateString()} +
+
+ {new Date(props.repo.updated_at).toLocaleDateString()}
-
- {repo.open_issues_count} issues +
+ {props.repo.open_issues_count} issues
diff --git a/components/ThemeToggle.tsx b/components/ThemeToggle.tsx index ba4cacd..0631b58 100644 --- a/components/ThemeToggle.tsx +++ b/components/ThemeToggle.tsx @@ -1,14 +1,12 @@ -import React from 'react'; -import { Sun, Moon, Monitor } from 'lucide-react'; -import { useTheme } from '../contexts/ThemeContext'; - -export const ThemeToggle: React.FC = () => { - const { theme, setTheme } = useTheme(); +import { Component } from 'solid-js'; +import { Sun, Moon, Monitor } from 'lucide-solid'; +import { store, setTheme } from '../store'; +export const ThemeToggle: Component = () => { const cycleTheme = () => { - if (theme === 'system') { + if (store.theme === 'system') { setTheme('light'); - } else if (theme === 'light') { + } else if (store.theme === 'light') { setTheme('dark'); } else { setTheme('system'); @@ -16,7 +14,7 @@ export const ThemeToggle: React.FC = () => { }; const getIcon = () => { - switch (theme) { + switch (store.theme) { case 'light': return ; case 'dark': @@ -27,7 +25,7 @@ export const ThemeToggle: React.FC = () => { }; const getLabel = () => { - switch (theme) { + switch (store.theme) { case 'light': return 'Light'; case 'dark': @@ -40,14 +38,11 @@ export const ThemeToggle: React.FC = () => { return ( ); -}; - - - +}; \ No newline at end of file diff --git a/components/Toast.tsx b/components/Toast.tsx index d5a42ff..ce754e0 100644 --- a/components/Toast.tsx +++ b/components/Toast.tsx @@ -1,5 +1,6 @@ -import React, { useEffect, useState } from 'react'; -import { X, CheckCircle2, AlertCircle, Info } from 'lucide-react'; +import { Component, For, Show, onMount, onCleanup } from 'solid-js'; +import { createMutable } from 'solid-js/store'; +import { X, CheckCircle2, AlertCircle, Info } from 'lucide-solid'; export type ToastType = 'success' | 'error' | 'info'; @@ -14,16 +15,16 @@ interface ToastProps { onDismiss: (id: string) => void; } -const Toast: React.FC = ({ toast, onDismiss }) => { - useEffect(() => { +const Toast: Component = (props) => { + onMount(() => { const timer = setTimeout(() => { - onDismiss(toast.id); + props.onDismiss(props.toast.id); }, 4000); - return () => clearTimeout(timer); - }, [toast.id, onDismiss]); + onCleanup(() => clearTimeout(timer)); + }); const getStyles = () => { - switch (toast.type) { + switch (props.toast.type) { case 'success': return 'bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-800 text-green-800 dark:text-green-200'; case 'error': @@ -35,27 +36,27 @@ const Toast: React.FC = ({ toast, onDismiss }) => { }; const getIcon = () => { - switch (toast.type) { + switch (props.toast.type) { case 'success': - return ; + return ; case 'error': - return ; + return ; case 'info': default: - return ; + return ; } }; return (