Created
June 18, 2025 05:55
-
-
Save xbalajipge/6e9e8d9bd0b7e457671cb3f8a2dd83e5 to your computer and use it in GitHub Desktop.
html-github-repos.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>GitHub Repositories</title> | |
| <style> | |
| /* Generated by Copilot */ | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| background: white; | |
| border-radius: 12px; | |
| box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); | |
| overflow: hidden; | |
| } | |
| .header { | |
| background: linear-gradient(135deg, #24292e 0%, #161b22 100%); | |
| color: white; | |
| padding: 30px; | |
| text-align: center; | |
| } | |
| .header h1 { | |
| font-size: 2.5rem; | |
| margin-bottom: 10px; | |
| font-weight: 300; | |
| } | |
| .header p { | |
| opacity: 0.8; | |
| font-size: 1.1rem; | |
| } | |
| .controls { | |
| padding: 20px 30px; | |
| background: #f8f9fa; | |
| border-bottom: 1px solid #e9ecef; | |
| display: flex; | |
| gap: 15px; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .username-input { | |
| display: flex; | |
| gap: 10px; | |
| align-items: center; | |
| } | |
| .username-input input, .username-input select { | |
| padding: 10px 12px; | |
| border: 2px solid #e9ecef; | |
| border-radius: 6px; | |
| font-size: 14px; | |
| transition: border-color 0.3s; | |
| } | |
| .username-input input:focus, .username-input select:focus { | |
| outline: none; | |
| border-color: #0366d6; | |
| } | |
| .pat-input { | |
| display: none; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| align-items: center; | |
| width: 100%; | |
| margin-top: 10px; | |
| padding: 15px; | |
| background: #fff3cd; | |
| border: 1px solid #ffeaa7; | |
| border-radius: 6px; | |
| } | |
| .pat-input.show { | |
| display: flex; | |
| } | |
| .pat-input input { | |
| flex-grow: 1; | |
| min-width: 300px; | |
| } | |
| .pat-help { | |
| font-size: 12px; | |
| color: #856404; | |
| flex-basis: 100%; | |
| margin-top: 5px; | |
| } | |
| .pat-help a { | |
| color: #0366d6; | |
| text-decoration: none; | |
| } | |
| .pat-help a:hover { | |
| text-decoration: underline; | |
| } | |
| .btn { | |
| padding: 10px 20px; | |
| background: #0366d6; | |
| color: white; | |
| border: none; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| transition: background-color 0.3s; | |
| } | |
| .btn:hover { | |
| background: #0256c7; | |
| } | |
| .btn:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| } | |
| .loading { | |
| text-align: center; | |
| padding: 50px; | |
| font-size: 1.1rem; | |
| color: #666; | |
| } | |
| .spinner { | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| border: 3px solid #f3f3f3; | |
| border-top: 3px solid #0366d6; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin-right: 10px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .error { | |
| background: #fee; | |
| color: #c33; | |
| padding: 20px; | |
| margin: 20px 30px; | |
| border-radius: 6px; | |
| border-left: 4px solid #c33; | |
| } | |
| .table-container { | |
| overflow-x: auto; | |
| padding: 0 30px 30px; | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin-top: 20px; | |
| } | |
| th, td { | |
| padding: 12px 15px; | |
| text-align: left; | |
| border-bottom: 1px solid #e9ecef; | |
| } | |
| th { | |
| background: #f8f9fa; | |
| font-weight: 600; | |
| cursor: pointer; | |
| user-select: none; | |
| position: relative; | |
| transition: background-color 0.3s; | |
| } | |
| th:hover { | |
| background: #e9ecef; | |
| } | |
| th.sortable::after { | |
| content: ' ↕'; | |
| opacity: 0.5; | |
| font-size: 12px; | |
| } | |
| th.sort-asc::after { | |
| content: ' ↑'; | |
| opacity: 1; | |
| } | |
| th.sort-desc::after { | |
| content: ' ↓'; | |
| opacity: 1; | |
| } | |
| tr:hover { | |
| background: #f8f9fa; | |
| } | |
| tr.private-repo { | |
| background: #fff8e1; | |
| border-left: 4px solid #ff9800; | |
| } | |
| tr.private-repo:hover { | |
| background: #fff3c4; | |
| } | |
| .repo-name { | |
| font-weight: 600; | |
| color: #0366d6; | |
| } | |
| .private-repo .repo-name { | |
| color: #e65100; | |
| } | |
| .repo-name a { | |
| color: inherit; | |
| text-decoration: none; | |
| } | |
| .repo-name a:hover { | |
| text-decoration: underline; | |
| } | |
| .language-tag { | |
| display: inline-block; | |
| padding: 2px 8px; | |
| background: #e1f5fe; | |
| color: #01579b; | |
| border-radius: 12px; | |
| font-size: 12px; | |
| font-weight: 500; | |
| } | |
| .stats { | |
| display: flex; | |
| gap: 15px; | |
| align-items: center; | |
| } | |
| .stat { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| font-size: 14px; | |
| color: #666; | |
| } | |
| .description { | |
| max-width: 300px; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .date { | |
| color: #666; | |
| font-size: 14px; | |
| } | |
| .repo-count { | |
| margin-left: auto; | |
| color: #666; | |
| font-size: 14px; | |
| } | |
| @media (max-width: 768px) { | |
| .controls { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| .username-input { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| th, td { | |
| padding: 8px 10px; | |
| font-size: 14px; | |
| } | |
| .description { | |
| max-width: 200px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>🐙 GitHub Repositories</h1> | |
| <p>Explore public repositories with sortable columns</p> | |
| </div> | |
| <div class="controls"> | |
| <div class="username-input"> | |
| <label for="username">GitHub Username:</label> | |
| <input type="text" id="username" placeholder="Enter GitHub username" /> | |
| <label for="repoType">Repository Type:</label> | |
| <select id="repoType" onchange="togglePATInput()"> | |
| <option value="public">Public Repositories</option> | |
| <option value="all">All Repositories</option> | |
| <option value="private">Private Repositories</option> | |
| </select> | |
| <button class="btn" onclick="fetchRepositories()">Load Repositories</button> | |
| </div> | |
| <div class="pat-input" id="patInput"> | |
| <label for="pat">Personal Access Token:</label> | |
| <input type="password" id="pat" placeholder="Enter your GitHub Personal Access Token" /> | |
| <div class="pat-help"> | |
| Required for private repositories. | |
| <a href="https://github.com/settings/tokens" target="_blank" rel="noopener noreferrer"> | |
| Generate a token here | |
| </a> with 'repo' scope. | |
| </div> | |
| </div> | |
| <div class="repo-count" id="repoCount"></div> | |
| </div> | |
| <div id="loading" class="loading" style="display: none;"> | |
| <div class="spinner"></div> | |
| Loading repositories... | |
| </div> | |
| <div id="error" class="error" style="display: none;"></div> | |
| <div class="table-container"> | |
| <table id="repoTable" style="display: none;"> | |
| <thead> | |
| <tr> | |
| <th class="sortable" data-column="name">Name</th> | |
| <th class="sortable" data-column="description">Description</th> | |
| <th class="sortable" data-column="language">Language</th> | |
| <th class="sortable" data-column="stars">Stars</th> | |
| <th class="sortable" data-column="forks">Forks</th> | |
| <th class="sortable" data-column="updated">Last Updated</th> | |
| <th class="sortable" data-column="created">Created</th> | |
| </tr> | |
| </thead> | |
| <tbody id="repoTableBody"> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <script> | |
| // Generated by Copilot | |
| let repositories = []; | |
| let currentSort = { column: null, direction: 'asc' }; | |
| // Initialize event listeners | |
| document.addEventListener('DOMContentLoaded', function() { | |
| document.getElementById('username').addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter') { | |
| fetchRepositories(); | |
| } | |
| }); | |
| document.getElementById('pat').addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter') { | |
| fetchRepositories(); | |
| } | |
| }); | |
| // Add click listeners to sortable headers | |
| document.querySelectorAll('th.sortable').forEach(header => { | |
| header.addEventListener('click', function() { | |
| const column = this.dataset.column; | |
| sortTable(column); | |
| }); | |
| }); | |
| }); | |
| function togglePATInput() { | |
| const repoType = document.getElementById('repoType').value; | |
| const patInput = document.getElementById('patInput'); | |
| if (repoType === 'all' || repoType === 'private') { | |
| patInput.classList.add('show'); | |
| } else { | |
| patInput.classList.remove('show'); | |
| } | |
| } | |
| async function fetchRepositories() { | |
| const username = document.getElementById('username').value.trim(); | |
| const repoType = document.getElementById('repoType').value; | |
| const pat = document.getElementById('pat').value.trim(); | |
| if (!username) { | |
| showError('Please enter a GitHub username'); | |
| return; | |
| } | |
| if ((repoType === 'all' || repoType === 'private') && !pat) { | |
| showError('Personal Access Token is required for accessing all repositories or private repositories'); | |
| return; | |
| } | |
| showLoading(true); | |
| hideError(); | |
| hideTable(); | |
| try { | |
| // Fetch repositories with pagination | |
| let allRepos = []; | |
| let page = 1; | |
| let hasMore = true; | |
| // Set up request headers | |
| const headers = { | |
| 'Accept': 'application/vnd.github.v3+json', | |
| 'User-Agent': 'GitHub-Repos-Viewer' | |
| }; | |
| if (pat) { | |
| headers['Authorization'] = `token ${pat}`; | |
| } | |
| // Determine API endpoint based on repo type | |
| let apiUrl; | |
| if (repoType === 'private') { | |
| apiUrl = `https://api.github.com/user/repos?visibility=private&per_page=100&page=`; | |
| } else if (repoType === 'all') { | |
| apiUrl = `https://api.github.com/user/repos?per_page=100&page=`; | |
| } else { | |
| apiUrl = `https://api.github.com/users/${username}/repos?per_page=100&page=`; | |
| } | |
| while (hasMore) { | |
| const response = await fetch(`${apiUrl}${page}&sort=updated`, { | |
| headers: headers | |
| }); | |
| if (!response.ok) { | |
| if (response.status === 404) { | |
| throw new Error('User not found. Please check the username.'); | |
| } else if (response.status === 401) { | |
| throw new Error('Invalid Personal Access Token. Please check your token and ensure it has the correct permissions.'); | |
| } else if (response.status === 403) { | |
| const rateLimitRemaining = response.headers.get('X-RateLimit-Remaining'); | |
| if (rateLimitRemaining === '0') { | |
| throw new Error('API rate limit exceeded. Please try again later or use a Personal Access Token for higher limits.'); | |
| } else { | |
| throw new Error('Access forbidden. Please check your permissions and token scope.'); | |
| } | |
| } else { | |
| throw new Error(`Error ${response.status}: ${response.statusText}`); | |
| } | |
| } | |
| const repos = await response.json(); | |
| if (repos.length === 0) { | |
| hasMore = false; | |
| } else { | |
| // Filter repos based on type and username if needed | |
| let filteredRepos = repos; | |
| if (repoType === 'private' || repoType === 'all') { | |
| // For authenticated requests, filter by owner if username is specified | |
| if (username) { | |
| filteredRepos = repos.filter(repo => | |
| repo.owner.login.toLowerCase() === username.toLowerCase() | |
| ); | |
| } | |
| } | |
| allRepos = allRepos.concat(filteredRepos); | |
| page++; | |
| // GitHub API returns max 100 per page, if we get less than 100, we're done | |
| if (repos.length < 100) { | |
| hasMore = false; | |
| } | |
| } | |
| } | |
| repositories = allRepos; | |
| displayRepositories(); | |
| updateRepoCount(); | |
| } catch (error) { | |
| showError(error.message); | |
| } finally { | |
| showLoading(false); | |
| } | |
| } | |
| function displayRepositories() { | |
| const tbody = document.getElementById('repoTableBody'); | |
| tbody.innerHTML = ''; | |
| repositories.forEach(repo => { | |
| const row = document.createElement('tr'); | |
| // Add private repo class for styling | |
| if (repo.private) { | |
| row.classList.add('private-repo'); | |
| } | |
| row.innerHTML = ` | |
| <td class="repo-name"> | |
| <a href="${repo.html_url}" target="_blank" rel="noopener noreferrer"> | |
| ${repo.name} ${repo.private ? '🔒' : ''} | |
| </a> | |
| </td> | |
| <td class="description" title="${repo.description || 'No description'}"> | |
| ${repo.description || 'No description'} | |
| </td> | |
| <td> | |
| ${repo.language ? `<span class="language-tag">${repo.language}</span>` : ''} | |
| </td> | |
| <td> | |
| <div class="stat"> | |
| ⭐ ${repo.stargazers_count} | |
| </div> | |
| </td> | |
| <td> | |
| <div class="stat"> | |
| 🔀 ${repo.forks_count} | |
| </div> | |
| </td> | |
| <td class="date"> | |
| ${formatDate(repo.updated_at)} | |
| </td> | |
| <td class="date"> | |
| ${formatDate(repo.created_at)} | |
| </td> | |
| `; | |
| tbody.appendChild(row); | |
| }); | |
| document.getElementById('repoTable').style.display = 'table'; | |
| } | |
| function sortTable(column) { | |
| // Update sort direction | |
| if (currentSort.column === column) { | |
| currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc'; | |
| } else { | |
| currentSort.column = column; | |
| currentSort.direction = 'asc'; | |
| } | |
| // Update header classes | |
| document.querySelectorAll('th.sortable').forEach(th => { | |
| th.classList.remove('sort-asc', 'sort-desc'); | |
| }); | |
| const currentHeader = document.querySelector(`th[data-column="${column}"]`); | |
| currentHeader.classList.add(currentSort.direction === 'asc' ? 'sort-asc' : 'sort-desc'); | |
| // Sort repositories | |
| repositories.sort((a, b) => { | |
| let aValue, bValue; | |
| switch (column) { | |
| case 'name': | |
| aValue = a.name.toLowerCase(); | |
| bValue = b.name.toLowerCase(); | |
| break; | |
| case 'description': | |
| aValue = (a.description || '').toLowerCase(); | |
| bValue = (b.description || '').toLowerCase(); | |
| break; | |
| case 'language': | |
| aValue = (a.language || '').toLowerCase(); | |
| bValue = (b.language || '').toLowerCase(); | |
| break; | |
| case 'stars': | |
| aValue = a.stargazers_count; | |
| bValue = b.stargazers_count; | |
| break; | |
| case 'forks': | |
| aValue = a.forks_count; | |
| bValue = b.forks_count; | |
| break; | |
| case 'updated': | |
| aValue = new Date(a.updated_at); | |
| bValue = new Date(b.updated_at); | |
| break; | |
| case 'created': | |
| aValue = new Date(a.created_at); | |
| bValue = new Date(b.created_at); | |
| break; | |
| default: | |
| return 0; | |
| } | |
| if (aValue < bValue) { | |
| return currentSort.direction === 'asc' ? -1 : 1; | |
| } | |
| if (aValue > bValue) { | |
| return currentSort.direction === 'asc' ? 1 : -1; | |
| } | |
| return 0; | |
| }); | |
| displayRepositories(); | |
| } | |
| function formatDate(dateString) { | |
| const date = new Date(dateString); | |
| return date.toLocaleDateString('en-US', { | |
| year: 'numeric', | |
| month: 'short', | |
| day: 'numeric' | |
| }); | |
| } | |
| function showLoading(show) { | |
| document.getElementById('loading').style.display = show ? 'block' : 'none'; | |
| } | |
| function showError(message) { | |
| const errorDiv = document.getElementById('error'); | |
| errorDiv.textContent = message; | |
| errorDiv.style.display = 'block'; | |
| } | |
| function hideError() { | |
| document.getElementById('error').style.display = 'none'; | |
| } | |
| function hideTable() { | |
| document.getElementById('repoTable').style.display = 'none'; | |
| } | |
| function updateRepoCount() { | |
| const count = repositories.length; | |
| const repoType = document.getElementById('repoType').value; | |
| const privateCount = repositories.filter(repo => repo.private).length; | |
| const publicCount = count - privateCount; | |
| let countText = `${count} repositories found`; | |
| if (repoType === 'all' && privateCount > 0) { | |
| countText += ` (${publicCount} public, ${privateCount} private)`; | |
| } else if (repoType === 'private') { | |
| countText = `${count} private repositories found`; | |
| } else { | |
| countText = `${count} public repositories found`; | |
| } | |
| document.getElementById('repoCount').textContent = countText; | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment