Learn Plain English logo

Learn Plain English

Hello there! I'm Iain Broome and I'm working on a new website that features courses and resources to help you write clearer content. Want to be the first to hear when it's ready? Pop your name on the list.

// Raindrop.io API Integration with Search, Filtering, and Pagination // Configuration const config = { accessToken: '7cf7856b-feb4-4428-9529-edcdbe3695d6', excludedCollections: ['Unread', 'Read Archive'], bookmarksPerPage: 20, sort: '-created' }; // State management let state = { allBookmarks: [], filteredBookmarks: [], collections: {}, activeFilter: null, currentPage: 1, totalPages: 1, searchQuery: '' }; // Format date as "25 January 2025" const formatDate = dateString => { const date = new Date(dateString); return `${date.getDate()} ${date.toLocaleString('en-US', {month: 'long'})} ${date.getFullYear()}`; }; // Extract root domain from URL const extractRootDomain = url => { try { if (!url) return ''; const parsedUrl = new URL(url); return parsedUrl.hostname; } catch (e) { return url; } }; // API functions async function fetchCollections() { try { const response = await fetch('https://api.raindrop.io/rest/v1/collections', { headers: { 'Authorization': `Bearer ${config.accessToken}` } }); if (!response.ok) throw new Error(`API request failed: ${response.status}`); const data = await response.json(); const collectionsMap = {}; data.items.forEach(collection => { if (!config.excludedCollections.includes(collection.title)) { collectionsMap[collection._id] = { id: collection._id, title: collection.title, count: collection.count }; } }); state.collections = collectionsMap; return collectionsMap; } catch (error) { console.error('Error fetching collections:', error); return {}; } } async function fetchAllBookmarks() { try { const collectionIds = Object.keys(state.collections).filter(id => id !== '0'); if (collectionIds.length === 0) return []; let allBookmarks = []; for (const collectionId of collectionIds) { const collection = state.collections[collectionId]; if (collection.count === 0) continue; const totalPages = Math.ceil(collection.count / 50); for (let page = 0; page < totalPages; page++) { const response = await fetch( `https://api.raindrop.io/rest/v1/raindrops/${collectionId}?sort=${config.sort}&perpage=50&page=${page}`, { headers: { 'Authorization': `Bearer ${config.accessToken}` } } ); if (!response.ok) continue; const data = await response.json(); const validBookmarks = data.items.filter(b => b.collection && b.collection.$id); allBookmarks = [...allBookmarks, ...validBookmarks]; await new Promise(resolve => setTimeout(resolve, 100)); // Avoid rate limiting } } allBookmarks.sort((a, b) => new Date(b.created) - new Date(a.created)); state.allBookmarks = allBookmarks; state.filteredBookmarks = allBookmarks; updatePagination(); return allBookmarks; } catch (error) { console.error('Error fetching bookmarks:', error); return []; } } // Filter and search functions function filterBookmarksByCollection(collectionId, filterElement) { state.activeFilter = collectionId; applyFilters(); if (filterElement) updateActiveFilter(filterElement); } function searchBookmarks(query) { state.searchQuery = query.trim().toLowerCase(); applyFilters(); } function applyFilters() { let filtered = state.allBookmarks; // Apply collection filter if (state.activeFilter !== null) { filtered = filtered.filter(b => b.collection && b.collection.$id === state.activeFilter); } // Apply search filter if (state.searchQuery) { filtered = filtered.filter(b => { const title = (b.title || '').toLowerCase(); const description = (b.description || '').toLowerCase(); const tags = (b.tags || []).join(' ').toLowerCase(); const url = (b.link || '').toLowerCase(); return title.includes(state.searchQuery) || description.includes(state.searchQuery) || tags.includes(state.searchQuery) || url.includes(state.searchQuery); }); } state.filteredBookmarks = filtered; state.currentPage = 1; updatePagination(); renderFilteredBookmarks(); } // Pagination functions function updatePagination() { state.totalPages = Math.ceil(state.filteredBookmarks.length / config.bookmarksPerPage); state.currentPage = Math.max(1, Math.min(state.currentPage, state.totalPages || 1)); } function getCurrentPageBookmarks() { const startIndex = (state.currentPage - 1) * config.bookmarksPerPage; return state.filteredBookmarks.slice(startIndex, startIndex + config.bookmarksPerPage); } function goToPage(pageNumber) { state.currentPage = Math.max(1, Math.min(pageNumber, state.totalPages)); renderFilteredBookmarks(); const bookmarkList = document.querySelector('.raindrop-bookmark-list'); if (bookmarkList) bookmarkList.scrollIntoView({ behavior: 'smooth' }); } // UI rendering functions function renderSearchBox() { const searchContainer = document.createElement('div'); searchContainer.className = 'raindrop-search-container'; const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.className = 'raindrop-search-input'; searchInput.placeholder = 'Search bookmarks...'; searchInput.value = state.searchQuery; // Add clear button if there's a search query if (state.searchQuery) { const clearButton = document.createElement('button'); clearButton.className = 'raindrop-search-clear'; clearButton.innerHTML = '×'; clearButton.title = 'Clear search'; clearButton.addEventListener('click', () => { searchInput.value = ''; searchBookmarks(''); }); searchContainer.appendChild(clearButton); } searchInput.addEventListener('input', e => searchBookmarks(e.target.value)); searchInput.addEventListener('keydown', e => { if (e.key === 'Enter') searchBookmarks(e.target.value); if (e.key === 'Escape') { searchInput.value = ''; searchBookmarks(''); } }); searchContainer.appendChild(searchInput); return searchContainer; } function renderCollectionFilters() { const filtersContainer = document.createElement('div'); filtersContainer.className = 'raindrop-collection-filters'; const filtersWrapper = document.createElement('div'); filtersWrapper.className = 'raindrop-collection-filters-wrapper'; // Add "All" filter const allFilter = document.createElement('span'); allFilter.className = `raindrop-collection-filter ${state.activeFilter === null ? 'active' : ''}`; allFilter.innerHTML = `All (${state.allBookmarks.length})`; allFilter.addEventListener('click', () => filterBookmarksByCollection(null, allFilter)); filtersWrapper.appendChild(allFilter); // Add collection filters const collectionsWithBookmarks = new Set(); state.allBookmarks.forEach(b => { if (b.collection && b.collection.$id) collectionsWithBookmarks.add(b.collection.$id); }); // Sort collections by title Object.values(state.collections) .filter(c => collectionsWithBookmarks.has(c.id)) .sort((a, b) => a.title.localeCompare(b.title)) .forEach(collection => { const bookmarkCount = state.allBookmarks.filter(b => b.collection && b.collection.$id === collection.id ).length; const filter = document.createElement('span'); filter.className = `raindrop-collection-filter ${state.activeFilter === collection.id ? 'active' : ''}`; filter.innerHTML = `${collection.title} (${bookmarkCount})`; filter.dataset.collectionId = collection.id; filter.addEventListener('click', () => filterBookmarksByCollection(collection.id, filter)); filtersWrapper.appendChild(filter); }); filtersContainer.appendChild(filtersWrapper); return filtersContainer; } function updateActiveFilter(activeElement) { document.querySelectorAll('.raindrop-collection-filter').forEach(f => f.classList.remove('active')); activeElement.classList.add('active'); } function highlightSearchTerms(element, query) { if (!query) return; const titleElement = element.querySelector('.raindrop-bookmark-title'); if (!titleElement) return; const text = titleElement.textContent; if (!text.toLowerCase().includes(query.toLowerCase())) return; const regex = new RegExp(`(${query})`, 'gi'); titleElement.innerHTML = text.replace(regex, '$1'); } function createBookmarkElement(bookmark) { const item = document.createElement('li'); item.className = 'raindrop-bookmark-item'; // Top row with link and collection const topRowContainer = document.createElement('div'); topRowContainer.className = 'raindrop-bookmark-top-row'; const link = document.createElement('a'); link.href = bookmark.link; link.target = '_blank'; link.className = 'raindrop-bookmark-link'; const titleContainer = document.createElement('div'); titleContainer.className = 'raindrop-bookmark-title-container'; if (bookmark.favicon) { const favicon = document.createElement('img'); favicon.src = bookmark.favicon; favicon.className = 'raindrop-bookmark-favicon'; favicon.alt = ''; titleContainer.appendChild(favicon); } const title = document.createElement('span'); title.className = 'raindrop-bookmark-title'; title.textContent = bookmark.title; titleContainer.appendChild(title); link.appendChild(titleContainer); topRowContainer.appendChild(link); if (bookmark.collection && state.collections[bookmark.collection.$id]) { const collectionSpan = document.createElement('span'); collectionSpan.className = 'raindrop-bookmark-collection'; collectionSpan.textContent = state.collections[bookmark.collection.$id].title; topRowContainer.appendChild(collectionSpan); } item.appendChild(topRowContainer); // Bottom row with date const bottomRowContainer = document.createElement('div'); bottomRowContainer.className = 'raindrop-bookmark-bottom-row'; const dateSpan = document.createElement('span'); dateSpan.className = 'raindrop-bookmark-date'; dateSpan.textContent = formatDate(bookmark.created); bottomRowContainer.appendChild(dateSpan); // Add URL for hover state const urlSpan = document.createElement('span'); urlSpan.className = 'raindrop-bookmark-url'; urlSpan.textContent = extractRootDomain(bookmark.link); bottomRowContainer.appendChild(urlSpan); item.appendChild(bottomRowContainer); // Highlight search terms if (state.searchQuery) highlightSearchTerms(item, state.searchQuery); return item; } function renderSearchInfo() { if (!state.searchQuery) return null; const searchInfo = document.createElement('div'); searchInfo.className = 'raindrop-search-info'; const resultCount = state.filteredBookmarks.length; searchInfo.textContent = resultCount === 1 ? `1 bookmark found for "${state.searchQuery}"` : `${resultCount} bookmarks found for "${state.searchQuery}"`; return searchInfo; } function renderPagination() { const paginationContainer = document.createElement('div'); paginationContainer.className = 'raindrop-pagination'; if (state.totalPages <= 1) return paginationContainer; // Page info const pageInfo = document.createElement('span'); pageInfo.className = 'raindrop-page-info'; pageInfo.textContent = `Page ${state.currentPage} of ${state.totalPages}`; paginationContainer.appendChild(pageInfo); // Pagination buttons const buttonContainer = document.createElement('div'); buttonContainer.className = 'raindrop-pagination-buttons'; const prevButton = document.createElement('button'); prevButton.className = 'raindrop-pagination-button'; prevButton.innerHTML = '« Previous'; prevButton.disabled = state.currentPage <= 1; prevButton.addEventListener('click', () => state.currentPage > 1 && goToPage(state.currentPage - 1)); const nextButton = document.createElement('button'); nextButton.className = 'raindrop-pagination-button'; nextButton.innerHTML = 'Next »'; nextButton.disabled = state.currentPage >= state.totalPages; nextButton.addEventListener('click', () => state.currentPage < state.totalPages && goToPage(state.currentPage + 1)); buttonContainer.appendChild(prevButton); buttonContainer.appendChild(nextButton); paginationContainer.appendChild(buttonContainer); return paginationContainer; } // Main rendering functions function renderFilteredBookmarks() { const container = document.getElementById('raindrop-bookmarks'); if (!container) return; // Get or create bookmark list let bookmarkList = container.querySelector('.raindrop-bookmark-list'); if (!bookmarkList) { bookmarkList = document.createElement('ul'); bookmarkList.className = 'raindrop-bookmark-list'; container.appendChild(bookmarkList); } else { bookmarkList.innerHTML = ''; } // Remove existing pagination and search info container.querySelectorAll('.raindrop-pagination-bottom, .raindrop-search-info').forEach(el => el.remove()); // Add search results info if searching if (state.searchQuery) { const searchInfo = renderSearchInfo(); if (searchInfo) bookmarkList.before(searchInfo); } // Handle no results if (state.filteredBookmarks.length === 0) { const noResults = document.createElement('li'); noResults.className = 'raindrop-no-results'; noResults.textContent = state.searchQuery ? `No bookmarks found for "${state.searchQuery}".` : 'No bookmarks found in this collection.'; bookmarkList.appendChild(noResults); return; } // Render bookmarks for current page getCurrentPageBookmarks().forEach(bookmark => { bookmarkList.appendChild(createBookmarkElement(bookmark)); }); // Add pagination at bottom const pagination = renderPagination(); pagination.className += ' raindrop-pagination-bottom'; container.appendChild(pagination); } function showLoadingIndicator() { const container = document.getElementById('raindrop-bookmarks'); if (!container) return; container.innerHTML = `

Just loading some bookmarks. There are quite a lot, so it may take a few seconds. Do not panic.

`; } function renderBookmarksList() { const container = document.getElementById('raindrop-bookmarks'); if (!container) return; container.innerHTML = ''; // Add search box container.appendChild(renderSearchBox()); // Add collection filters container.appendChild(renderCollectionFilters()); // Add search info if searching if (state.searchQuery) { const searchInfo = renderSearchInfo(); if (searchInfo) container.appendChild(searchInfo); } // Add bookmark list const bookmarkList = document.createElement('ul'); bookmarkList.className = 'raindrop-bookmark-list'; container.appendChild(bookmarkList); // Handle no results if (state.filteredBookmarks.length === 0) { const noResults = document.createElement('li'); noResults.className = 'raindrop-no-results'; noResults.textContent = state.searchQuery ? `No bookmarks found for "${state.searchQuery}".` : 'No bookmarks found in this collection.'; bookmarkList.appendChild(noResults); return; } // Render current page bookmarks getCurrentPageBookmarks().forEach(bookmark => { bookmarkList.appendChild(createBookmarkElement(bookmark)); }); // Add pagination const pagination = renderPagination(); pagination.className += ' raindrop-pagination-bottom'; container.appendChild(pagination); } // Initialization async function initBookmarks(containerId) { const container = document.getElementById(containerId); if (!container) { console.error(`Container not found: ${containerId}`); return; } // Set container styles Object.assign(container.style, { width: '100%', maxWidth: '100%', overflow: 'hidden', boxSizing: 'border-box', padding: '0', margin: '0' }); sho
Learn Plain English logo

Thank you!

How exciting! Look out for all the usual sign-up gubbins in your inbox and I'll be in touch as soon as there is something to share.