-
+
{repo.name}
diff --git a/app/components/@settings/tabs/connections/components/GitHubAuthDialog.tsx b/app/components/@settings/tabs/connections/components/GitHubAuthDialog.tsx
new file mode 100644
index 0000000..b53a64d
--- /dev/null
+++ b/app/components/@settings/tabs/connections/components/GitHubAuthDialog.tsx
@@ -0,0 +1,190 @@
+import React, { useState } from 'react';
+import * as Dialog from '@radix-ui/react-dialog';
+import { motion } from 'framer-motion';
+import { toast } from 'react-toastify';
+import Cookies from 'js-cookie';
+import type { GitHubUserResponse } from '~/types/GitHub';
+
+interface GitHubAuthDialogProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export function GitHubAuthDialog({ isOpen, onClose }: GitHubAuthDialogProps) {
+ const [token, setToken] = useState('');
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [tokenType, setTokenType] = useState<'classic' | 'fine-grained'>('classic');
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!token.trim()) {
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ const response = await fetch('https://api.github.com/user', {
+ headers: {
+ Accept: 'application/vnd.github.v3+json',
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ if (response.ok) {
+ const userData = (await response.json()) as GitHubUserResponse;
+
+ // Save connection data
+ const connectionData = {
+ token,
+ tokenType,
+ user: {
+ login: userData.login,
+ avatar_url: userData.avatar_url,
+ name: userData.name || userData.login,
+ },
+ connected_at: new Date().toISOString(),
+ };
+
+ localStorage.setItem('github_connection', JSON.stringify(connectionData));
+
+ // Set cookies for API requests
+ Cookies.set('githubToken', token);
+ Cookies.set('githubUsername', userData.login);
+ Cookies.set('git:github.com', JSON.stringify({ username: token, password: 'x-oauth-basic' }));
+
+ toast.success(`Successfully connected as ${userData.login}`);
+ setToken('');
+ onClose();
+ } else {
+ if (response.status === 401) {
+ toast.error('Invalid GitHub token. Please check and try again.');
+ } else {
+ toast.error(`GitHub API error: ${response.status} ${response.statusText}`);
+ }
+ }
+ } catch (error) {
+ console.error('Error connecting to GitHub:', error);
+ toast.error('Failed to connect to GitHub. Please try again.');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
!open && onClose()}>
+
+
+
+
+
+
+
Access Private Repositories
+
+
+ To access private repositories, you need to connect your GitHub account by providing a personal access
+ token.
+
+
+
+
Connect with GitHub Token
+
+
+
+
+
+
+
+ Accessing Private Repositories
+
+
+ Important things to know about accessing private repositories:
+
+
+ - You must be granted access to the repository by its owner
+ - Your GitHub token must have the 'repo' scope
+ - For organization repositories, you may need additional permissions
+ - No token can give you access to repositories you don't have permission for
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/components/@settings/tabs/connections/components/PushToGitHubDialog.tsx b/app/components/@settings/tabs/connections/components/PushToGitHubDialog.tsx
index fe7f9f4..1f8adb9 100644
--- a/app/components/@settings/tabs/connections/components/PushToGitHubDialog.tsx
+++ b/app/components/@settings/tabs/connections/components/PushToGitHubDialog.tsx
@@ -1,7 +1,10 @@
import * as Dialog from '@radix-ui/react-dialog';
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
import { toast } from 'react-toastify';
import { motion } from 'framer-motion';
+import { Octokit } from '@octokit/rest';
+
+// Internal imports
import { getLocalStorage } from '~/lib/persistence';
import { classNames } from '~/utils/classNames';
import type { GitHubUserResponse } from '~/types/GitHub';
@@ -10,7 +13,9 @@ import { workbenchStore } from '~/lib/stores/workbench';
import { extractRelativePath } from '~/utils/diff';
import { formatSize } from '~/utils/formatSize';
import type { FileMap, File } from '~/lib/stores/files';
-import { Octokit } from '@octokit/rest';
+
+// UI Components
+import { Badge, EmptyState, StatusIndicator, SearchInput } from '~/components/ui';
interface PushToGitHubDialogProps {
isOpen: boolean;
@@ -37,6 +42,8 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
const [isLoading, setIsLoading] = useState(false);
const [user, setUser] = useState
(null);
const [recentRepos, setRecentRepos] = useState([]);
+ const [filteredRepos, setFilteredRepos] = useState([]);
+ const [repoSearchQuery, setRepoSearchQuery] = useState('');
const [isFetchingRepos, setIsFetchingRepos] = useState(false);
const [showSuccessDialog, setShowSuccessDialog] = useState(false);
const [createdRepoUrl, setCreatedRepoUrl] = useState('');
@@ -58,7 +65,34 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
}
}, [isOpen]);
- const fetchRecentRepos = async (token: string) => {
+ /*
+ * Filter repositories based on search query
+ * const debouncedSetRepoSearchQuery = useDebouncedCallback((value: string) => setRepoSearchQuery(value), 300);
+ */
+
+ useEffect(() => {
+ if (recentRepos.length === 0) {
+ setFilteredRepos([]);
+ return;
+ }
+
+ if (!repoSearchQuery.trim()) {
+ setFilteredRepos(recentRepos);
+ return;
+ }
+
+ const query = repoSearchQuery.toLowerCase().trim();
+ const filtered = recentRepos.filter(
+ (repo) =>
+ repo.name.toLowerCase().includes(query) ||
+ (repo.description && repo.description.toLowerCase().includes(query)) ||
+ (repo.language && repo.language.toLowerCase().includes(query)),
+ );
+
+ setFilteredRepos(filtered);
+ }, [recentRepos, repoSearchQuery]);
+
+ const fetchRecentRepos = useCallback(async (token: string) => {
if (!token) {
logStore.logError('No GitHub token available');
toast.error('GitHub authentication required');
@@ -68,53 +102,89 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
try {
setIsFetchingRepos(true);
+ console.log('Fetching GitHub repositories with token:', token.substring(0, 5) + '...');
- const response = await fetch(
- 'https://api.github.com/user/repos?sort=updated&per_page=5&type=all&affiliation=owner,organization_member',
- {
+ // Fetch ALL repos by paginating through all pages
+ let allRepos: GitHubRepo[] = [];
+ let page = 1;
+ let hasMore = true;
+
+ while (hasMore) {
+ const requestUrl = `https://api.github.com/user/repos?sort=updated&per_page=100&page=${page}&affiliation=owner,organization_member`;
+ const response = await fetch(requestUrl, {
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `Bearer ${token.trim()}`,
},
- },
- );
+ });
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({}));
+ if (!response.ok) {
+ let errorData: { message?: string } = {};
- if (response.status === 401) {
- toast.error('GitHub token expired. Please reconnect your account.');
-
- // Clear invalid token
- const connection = getLocalStorage('github_connection');
-
- if (connection) {
- localStorage.removeItem('github_connection');
- setUser(null);
+ try {
+ errorData = await response.json();
+ console.error('Error response data:', errorData);
+ } catch (e) {
+ errorData = { message: 'Could not parse error response' };
+ console.error('Could not parse error response:', e);
}
- } else {
- logStore.logError('Failed to fetch GitHub repositories', {
- status: response.status,
- statusText: response.statusText,
- error: errorData,
- });
- toast.error(`Failed to fetch repositories: ${response.statusText}`);
+
+ if (response.status === 401) {
+ toast.error('GitHub token expired. Please reconnect your account.');
+
+ // Clear invalid token
+ const connection = getLocalStorage('github_connection');
+
+ if (connection) {
+ localStorage.removeItem('github_connection');
+ setUser(null);
+ }
+ } else if (response.status === 403 && response.headers.get('x-ratelimit-remaining') === '0') {
+ // Rate limit exceeded
+ const resetTime = response.headers.get('x-ratelimit-reset');
+ const resetDate = resetTime ? new Date(parseInt(resetTime) * 1000).toLocaleTimeString() : 'soon';
+ toast.error(`GitHub API rate limit exceeded. Limit resets at ${resetDate}`);
+ } else {
+ logStore.logError('Failed to fetch GitHub repositories', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorData,
+ });
+ toast.error(`Failed to fetch repositories: ${errorData.message || response.statusText}`);
+ }
+
+ return;
}
- return;
- }
+ try {
+ const repos = (await response.json()) as GitHubRepo[];
+ allRepos = allRepos.concat(repos);
- const repos = (await response.json()) as GitHubRepo[];
- setRecentRepos(repos);
+ if (repos.length < 100) {
+ hasMore = false;
+ } else {
+ page += 1;
+ }
+ } catch (parseError) {
+ console.error('Error parsing JSON response:', parseError);
+ logStore.logError('Failed to parse GitHub repositories response', { parseError });
+ toast.error('Failed to parse repository data');
+ setRecentRepos([]);
+
+ return;
+ }
+ }
+ setRecentRepos(allRepos);
} catch (error) {
+ console.error('Exception while fetching GitHub repositories:', error);
logStore.logError('Failed to fetch GitHub repositories', { error });
toast.error('Failed to fetch recent repositories');
} finally {
setIsFetchingRepos(false);
}
- };
+ }, []);
- const handleSubmit = async (e: React.FormEvent) => {
+ async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const connection = getLocalStorage('github_connection');
@@ -186,7 +256,7 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
} finally {
setIsLoading(false);
}
- };
+ }
const handleClose = () => {
setRepoName('');
@@ -210,27 +280,46 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
transition={{ duration: 0.2 }}
className="w-[90vw] md:w-[600px] max-h-[85vh] overflow-y-auto"
>
-
+
-
-
-
Successfully pushed to GitHub
+
+
+
+
+ Successfully pushed to GitHub
+
+
+ Your code is now available on GitHub
+
+
-
-
+
+
-
-
+
-
-
+
+
+
Pushed Files ({pushedFiles.length})
-
+
{pushedFiles.map((file) => (
- {file.path}
-
+ {file.path}
+
{formatSize(file.size)}
@@ -283,7 +373,7 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
navigator.clipboard.writeText(createdRepoUrl);
toast.success('URL copied to clipboard');
}}
- className="px-4 py-2 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] text-gray-600 dark:text-gray-400 hover:bg-[#E5E5E5] dark:hover:bg-[#252525] text-sm inline-flex items-center gap-2"
+ className="px-4 py-2 rounded-lg bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary-dark hover:bg-bolt-elements-background-depth-3 dark:hover:bg-bolt-elements-background-depth-4 text-sm inline-flex items-center gap-2 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
@@ -292,7 +382,7 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
@@ -321,29 +411,57 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
transition={{ duration: 0.2 }}
className="w-[90vw] md:w-[500px]"
>
-
-
+
+
+
+
+
-
+
-
GitHub Connection Required
-
- Please connect your GitHub account in Settings {'>'} Connections to push your code to GitHub.
-
-
+ GitHub Connection Required
+
+
-
- Close
-
+ To push your code to GitHub, you need to connect your GitHub account in Settings {'>'} Connections
+ first.
+
+
+
+ Close
+
+
+
+ Go to Settings
+
+
@@ -365,7 +483,10 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
transition={{ duration: 0.2 }}
className="w-[90vw] md:w-[500px]"
>
-
+
-
+
-
+
Push to GitHub
-
+
Push your code to a new or existing GitHub repository
-
-
+
+
-
-

+
+
+

+
+
-
{user.name || user.login}
-
@{user.login}
+
+ {user.name || user.login}
+
+
+ @{user.login}
+