Created
November 26, 2024 20:17
-
-
Save georgeck/baacd2bc8268a7ae598773036f89ca73 to your computer and use it in GitHub Desktop.
This file contains 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"> | |
<meta name="description" content="A collection of my saved bookmarks"> | |
<title>My Bookmarks</title> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" integrity="sha512-wnea99uKIC3TJF7v4eKk4Y+lMz2Mklv18+r4na2Gn1abDRPPOeef95xTzdwGD9e6zXJBteMIhZ1+68QC5byJZw==" crossorigin="anonymous" referrerpolicy="no-referrer" /> | |
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📚</text></svg>"> | |
<style> | |
.external-link::after { | |
content: "↗"; | |
display: inline-block; | |
margin-left: 4px; | |
} | |
.bookmark-card { | |
transition: transform 0.2s; | |
} | |
.bookmark-card:hover { | |
transform: translateY(-2px); | |
} | |
.loading-spinner { | |
border: 3px solid #f3f3f3; | |
border-top: 3px solid #3498db; | |
border-radius: 50%; | |
width: 24px; | |
height: 24px; | |
animation: spin 1s linear infinite; | |
} | |
@keyframes spin { | |
0% { transform: rotate(0deg); } | |
100% { transform: rotate(360deg); } | |
} | |
/* Dark mode support */ | |
@media (prefers-color-scheme: dark) { | |
body { | |
background-color: #1a1a1a; | |
color: #e5e5e5; | |
} | |
.bookmark-card { | |
background-color: #2d2d2d; | |
border-color: #404040; | |
} | |
.external-link { | |
color: #60a5fa; | |
} | |
} | |
</style> | |
</head> | |
<body class="bg-gray-100 min-h-screen dark:bg-gray-900"> | |
<div class="container mx-auto px-4 py-8"> | |
<header class="mb-8"> | |
<h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100">My Bookmarks</h1> | |
</header> | |
<!-- Loading State --> | |
<div id="loading" class="hidden flex justify-center items-center py-8"> | |
<div class="loading-spinner"></div> | |
<span class="ml-3 text-gray-600 dark:text-gray-400">Loading bookmarks...</span> | |
</div> | |
<!-- Error State --> | |
<div id="error" class="hidden bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-6 dark:bg-red-900 dark:text-red-100" role="alert"> | |
<span class="block sm:inline" id="error-message"></span> | |
</div> | |
<!-- Bookmarks Container --> | |
<div id="bookmarks-container" class="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3"> | |
<!-- Bookmarks will be inserted here --> | |
</div> | |
<!-- Load More Button --> | |
<div id="load-more-container" class="mt-8 text-center"> | |
<button id="load-more" class="hidden bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded transition duration-200"> | |
Load More | |
</button> | |
</div> | |
</div> | |
<script> | |
const API_URL = 'https://bookmarks.georgeck.workers.dev/bookmarks'; | |
let isLoading = false; | |
// Utility function to format timestamps | |
function formatTimestamp(timestamp) { | |
const date = new Date(timestamp); | |
return new Intl.DateTimeFormat('en-US', { | |
year: 'numeric', | |
month: 'short', | |
day: 'numeric', | |
hour: '2-digit', | |
minute: '2-digit' | |
}).format(date); | |
} | |
// Function to create a bookmark card | |
function createBookmarkCard(bookmark) { | |
const card = document.createElement('div'); | |
card.className = 'bookmark-card bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden'; | |
const content = ` | |
<div class="p-6"> | |
<div class="mb-4"> | |
<a href="${bookmark.url}" target="_blank" rel="noopener noreferrer" | |
class="text-xl font-semibold text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 external-link"> | |
${bookmark.title} | |
</a> | |
</div> | |
<div class="bookmark-content mb-4"> | |
${bookmark.html} | |
</div> | |
<div class="flex justify-between items-center text-sm text-gray-600 dark:text-gray-400"> | |
<div> | |
<a href="${bookmark.author_url}" target="_blank" rel="noopener noreferrer" | |
class="hover:text-blue-600 dark:hover:text-blue-400 external-link"> | |
${bookmark.author_name} | |
</a> | |
</div> | |
<time datetime="${bookmark.saved_at}">${formatTimestamp(bookmark.saved_at)}</time> | |
</div> | |
</div> | |
`; | |
card.innerHTML = content; | |
return card; | |
} | |
// Function to handle loading state | |
function setLoading(loading) { | |
isLoading = loading; | |
const loadingElement = document.getElementById('loading'); | |
const loadMoreButton = document.getElementById('load-more'); | |
loadingElement.classList.toggle('hidden', !loading); | |
if (loadMoreButton) { | |
loadMoreButton.disabled = loading; | |
loadMoreButton.classList.toggle('opacity-50', loading); | |
} | |
} | |
// Function to show error message | |
function showError(message) { | |
const errorElement = document.getElementById('error'); | |
const errorMessageElement = document.getElementById('error-message'); | |
errorElement.classList.remove('hidden'); | |
errorMessageElement.textContent = message; | |
} | |
// Function to hide error message | |
function hideError() { | |
const errorElement = document.getElementById('error'); | |
errorElement.classList.add('hidden'); | |
} | |
// Function to fetch and display bookmarks | |
async function fetchBookmarks() { | |
if (isLoading) return; | |
setLoading(true); | |
hideError(); | |
try { | |
const response = await fetch(API_URL); | |
if (!response.ok) { | |
throw new Error('Failed to fetch bookmarks'); | |
} | |
const data = await response.json(); | |
const bookmarksContainer = document.getElementById('bookmarks-container'); | |
const loadMoreButton = document.getElementById('load-more'); | |
data.bookmarks.forEach(bookmark => { | |
const card = createBookmarkCard(bookmark); | |
bookmarksContainer.appendChild(card); | |
}); | |
loadMoreButton.classList.toggle('hidden', !data.has_more); | |
// Initialize oEmbed content | |
initializeOembeds(); | |
} catch (error) { | |
console.error('Error fetching bookmarks:', error); | |
showError('Failed to load bookmarks. Please try again later.'); | |
} finally { | |
setLoading(false); | |
} | |
} | |
// Function to initialize oEmbed content | |
function initializeOembeds() { | |
const scripts = document.querySelectorAll('.bookmark-content script'); | |
scripts.forEach(script => { | |
if (script.src) { | |
const newScript = document.createElement('script'); | |
newScript.src = script.src; | |
newScript.async = true; | |
document.body.appendChild(newScript); | |
} | |
}); | |
} | |
// Event Listeners | |
document.addEventListener('DOMContentLoaded', fetchBookmarks); | |
document.getElementById('load-more').addEventListener('click', fetchBookmarks); | |
// Handle scroll-based loading | |
let scrollTimeout; | |
window.addEventListener('scroll', () => { | |
clearTimeout(scrollTimeout); | |
scrollTimeout = setTimeout(() => { | |
const { scrollTop, scrollHeight, clientHeight } = document.documentElement; | |
if (scrollTop + clientHeight >= scrollHeight - 100) { | |
const loadMoreButton = document.getElementById('load-more'); | |
if (!loadMoreButton.classList.contains('hidden')) { | |
fetchBookmarks(); | |
} | |
} | |
}, 100); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment