Files
at-container-registry/pkg/appview/static/js/app.js

284 lines
8.8 KiB
JavaScript

// Copy to clipboard
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
// Show success feedback
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = '✓ Copied!';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
});
}
// Time ago helper (for client-side rendering)
function timeAgo(date) {
const seconds = Math.floor((new Date() - new Date(date)) / 1000);
const intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1
};
for (const [name, secondsInInterval] of Object.entries(intervals)) {
const interval = Math.floor(seconds / secondsInInterval);
if (interval >= 1) {
return interval === 1 ? `1 ${name} ago` : `${interval} ${name}s ago`;
}
}
return 'just now';
}
// Update timestamps on page load and HTMX swaps
function updateTimestamps() {
document.querySelectorAll('time[datetime]').forEach(el => {
const date = el.getAttribute('datetime');
if (date && !el.dataset.noUpdate) {
const ago = timeAgo(date);
if (el.textContent !== ago) {
el.textContent = ago;
}
}
});
}
// Initial timestamp update
document.addEventListener('DOMContentLoaded', updateTimestamps);
// Update timestamps after HTMX swaps
document.addEventListener('htmx:afterSwap', updateTimestamps);
// Update timestamps periodically
setInterval(updateTimestamps, 60000); // Every minute
// Toggle repository details (for images page)
function toggleRepo(name) {
const details = document.getElementById('repo-' + name);
const btn = document.getElementById('btn-' + name);
if (details.style.display === 'none') {
details.style.display = 'block';
btn.textContent = '▲';
} else {
details.style.display = 'none';
btn.textContent = '▼';
}
}
// User dropdown menu
document.addEventListener('DOMContentLoaded', () => {
const menuBtn = document.getElementById('user-menu-btn');
const dropdownMenu = document.getElementById('user-dropdown-menu');
if (menuBtn && dropdownMenu) {
// Toggle dropdown on button click
menuBtn.addEventListener('click', (e) => {
e.stopPropagation();
const isExpanded = menuBtn.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
closeDropdown();
} else {
openDropdown();
}
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!menuBtn.contains(e.target) && !dropdownMenu.contains(e.target)) {
closeDropdown();
}
});
// Close dropdown on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeDropdown();
}
});
function openDropdown() {
menuBtn.setAttribute('aria-expanded', 'true');
dropdownMenu.removeAttribute('hidden');
}
function closeDropdown() {
menuBtn.setAttribute('aria-expanded', 'false');
dropdownMenu.setAttribute('hidden', '');
}
}
});
// Toggle star on a repository
async function toggleStar(handle, repository) {
const starBtn = document.getElementById('star-btn');
const starIcon = document.getElementById('star-icon');
const starCountEl = document.getElementById('star-count');
if (!starBtn || !starIcon || !starCountEl) return;
// Disable button during request
starBtn.disabled = true;
try {
// Check current state
const isStarred = starIcon.textContent === '★';
const method = isStarred ? 'DELETE' : 'POST';
const url = `/api/stars/${handle}/${repository}`;
const response = await fetch(url, {
method: method,
credentials: 'include',
});
if (response.status === 401) {
console.log('Not authenticated, redirecting to login');
// Not authenticated, redirect to login
window.location.href = '/auth/oauth/login';
return;
}
if (!response.ok) {
const errorText = await response.text();
console.error(`Toggle star failed: ${response.status} ${response.statusText}`, errorText);
throw new Error(`Failed to toggle star: ${errorText}`);
}
const data = await response.json();
// Update UI optimistically
if (data.starred) {
starIcon.textContent = '★';
starBtn.classList.add('starred');
// Optimistically increment count
const currentCount = parseInt(starCountEl.textContent) || 0;
starCountEl.textContent = currentCount + 1;
} else {
starIcon.textContent = '☆';
starBtn.classList.remove('starred');
// Optimistically decrement count
const currentCount = parseInt(starCountEl.textContent) || 0;
starCountEl.textContent = Math.max(0, currentCount - 1);
}
// Don't fetch count immediately - trust the optimistic update
// The actual count will be correct on next page load
} catch (err) {
console.error('Error toggling star:', err);
alert(`Failed to toggle star: ${err.message}`);
} finally {
starBtn.disabled = false;
}
}
// Load star status and count for current repository
async function loadStarStatus() {
const starBtn = document.getElementById('star-btn');
const starIcon = document.getElementById('star-icon');
if (!starBtn || !starIcon) return; // Not on repository page
// Extract handle and repository from button onclick attribute
const onclick = starBtn.getAttribute('onclick');
const match = onclick.match(/toggleStar\('([^']+)',\s*'([^']+)'\)/);
if (!match) return;
const handle = match[1];
const repository = match[2];
try {
// Check if user has starred this repo
const starResponse = await fetch(`/api/stars/${handle}/${repository}`, {
credentials: 'include',
});
if (starResponse.ok) {
const starData = await starResponse.json();
console.log('Star status data:', starData);
if (starData.starred) {
starIcon.textContent = '★';
starBtn.classList.add('starred');
}
} else {
const errorText = await starResponse.text();
console.error('Failed to load star status:', errorText);
}
// Load star count
await loadStarCount(handle, repository);
} catch (err) {
console.error('Error loading star status:', err);
}
}
// Load star count for a repository
async function loadStarCount(handle, repository) {
const starCountEl = document.getElementById('star-count');
if (!starCountEl) return;
try {
const statsResponse = await fetch(`/api/stats/${handle}/${repository}`, {
credentials: 'include',
});
if (statsResponse.ok) {
const stats = await statsResponse.json();
console.log('Stats data:', stats);
starCountEl.textContent = stats.star_count || 0;
} else {
const errorText = await statsResponse.text();
console.error('Failed to load stats:', errorText);
}
} catch (err) {
console.error('Error loading star count:', err);
}
}
// Toggle offline manifests visibility
function toggleOfflineManifests() {
const checkbox = document.getElementById('show-offline-toggle');
const manifestsList = document.querySelector('.manifests-list');
if (!checkbox || !manifestsList) return;
// Store preference in localStorage
localStorage.setItem('showOfflineManifests', checkbox.checked);
// Toggle visibility of offline manifests
if (checkbox.checked) {
manifestsList.classList.add('show-offline');
} else {
manifestsList.classList.remove('show-offline');
}
}
// Restore offline manifests toggle state on page load
document.addEventListener('DOMContentLoaded', () => {
const checkbox = document.getElementById('show-offline-toggle');
if (!checkbox) return;
// Restore state from localStorage
const showOffline = localStorage.getItem('showOfflineManifests') === 'true';
checkbox.checked = showOffline;
// Apply initial state
const manifestsList = document.querySelector('.manifests-list');
if (manifestsList) {
if (showOffline) {
manifestsList.classList.add('show-offline');
} else {
manifestsList.classList.remove('show-offline');
}
}
});