Skip to content

Instantly share code, notes, and snippets.

@xbalajipge
Created June 18, 2025 05:55
Show Gist options
  • Select an option

  • Save xbalajipge/6e9e8d9bd0b7e457671cb3f8a2dd83e5 to your computer and use it in GitHub Desktop.

Select an option

Save xbalajipge/6e9e8d9bd0b7e457671cb3f8a2dd83e5 to your computer and use it in GitHub Desktop.
html-github-repos.html
<!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