// 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); } }