1// mediumService.js 2 3 async function loadMediumJSON(options = {}) { 4 const { 5 limit = null, 6 cacheMinutes = 30 7 } = options; 8 9 const cacheKey = "mediumFeedCache"; 10 const cacheTTL = cacheMinutes * 60 * 1000; 11 12 const cached = getCachedFeed(cacheKey, cacheTTL); 13 14 // ð¥ If cached exists, return immediately (fast UI) 15 if (cached) { 16 // Trigger background refresh (donât await) 17 refreshInBackground(cacheKey); 18 19 return applyLimit(cached, limit); 20 } 21 22 // No cache â fetch normally 23 const fresh = await fetchMediumFeed(); 24 setCachedFeed(cacheKey, fresh); 25 26 return applyLimit(fresh, limit); 27} 28 29async function refreshInBackground(cacheKey) { 30 try { 31 const freshData = await fetchMediumFeed(); 32 const current = getCachedFeed(cacheKey, Infinity); 33 34 // Compare JSON to detect change 35 if (JSON.stringify(freshData) !== JSON.stringify(current)) { 36 setCachedFeed(cacheKey, freshData); 37 38 // ð Notify the app that new data is available 39 window.dispatchEvent( 40 new CustomEvent("mediumFeedUpdated", { 41 detail: freshData 42 }) 43 ); 44 } 45 } catch (err) { 46 console.log("Background refresh failed (ignored):", err); 47 } 48} 49function getCachedFeed(key, ttl) { 50 const stored = localStorage.getItem(key); 51 52 if (!stored) return null; 53 54 const parsed = JSON.parse(stored); 55 56 if (Date.now() - parsed.timestamp > ttl) { 57 localStorage.removeItem(key); 58 return null; 59 } 60 61 return parsed.data; 62} 63 64function setCachedFeed(key, data) { 65 const payload = { 66 timestamp: Date.now(), 67 data: data 68 }; 69 70 localStorage.setItem(key, JSON.stringify(payload)); 71} 72function applyLimit(array, limit) { 73 if (!limit) return array; 74 return array.slice(0, limit); 75} 76 77async function fetchMediumFeed() { 78 const mediumNames = ["@hhhk_83035"]; 79 const endpoint = "https://api.rss2json.com/v1/api.json"; 80 81 const responses = await Promise.all( 82 mediumNames.map(name => 83 fetch(`${endpoint}?rss_url=${encodeURIComponent( 84 `https://medium.com/feed/${name}` 85 )}`) 86 ) 87 ); 88 89 const jsonData = await Promise.all(responses.map(r => r.json())); 90 91 const articles = []; 92 93 jsonData.forEach(feed => { 94 if (feed.status === "ok") { 95 feed.items.forEach(item => { 96 articles.push(transformItem(item)); 97 }); 98 } 99 }); 100 101 articles.sort((a, b) => new Date(b.rawDate) - new Date(a.rawDate)); 102 103 return articles; 104} 105 106function truncateText(text, maxLength) { 107 if (text.length <= maxLength) return text; 108 109 const trimmed = text.substring(0, maxLength); 110 return trimmed.substring(0, trimmed.lastIndexOf(" ")) + "..."; 111} 112 113function transformItem(item) { 114 const cleanDescription = stripHtml(item.description); 115 116 return { 117 title: item.title, 118 type: "Blog", 119 date: item.pubDate, 120 categories: ["blogs"], 121 image: extractImage(item.description), 122 description: cleanDescription, 123 url: item.link 124 }; 125} 126function stripHtml(html) { 127 const div = document.createElement("div"); 128 div.innerHTML = html; 129 return div.textContent || div.innerText || ""; 130} 131 132function extractImage(description) { 133 const match = description.match(/<img[^>]+src="([^">]+)"/); 134 return match ? match[1] : "images/resources/radiator.png"; 135} 136 137function formatDate(dateString) { 138 const date = new Date(dateString); 139 return date.toLocaleDateString("en-GB", { 140 day: "numeric", 141 month: "long", 142 year: "numeric" 143 }); 144} 145async function loadMedium() { 146 147 const mediumItems = await loadMediumJSON({ 148 limit: 2, // Show only the latest 5 items 149 }); 150 const grid = document.getElementById('researchGrid'); 151 grid.innerHTML = ''; // Clear existing content 152 153 mediumItems.forEach(item => { 154 const card = document.createElement('article'); 155 card.className = `frontpage-card ${item.categories.join(' ')}`; 156 157 card.innerHTML = ` 158 <div class="frontcard-image">
159 <span class="card-badge">${item.type}</span> 160 <img src="${item.image}" alt="${item.title}"> 161 </div> 162 <div class="card-content"> 163 <h4>${item.title}</h4> 164 <p>${item.date ? `<span class="icon fa-regular fa-calendar accent1"></span> ${item.date}` : ''}</p> 165 <p>${truncateText(item.description, 250)}</p> 166 <a href="${item.url}" target="_blank" rel="noopener">Read more â</a> 167 </div> 168 `; 169 170 grid.appendChild(card); 171 }); 172} 173 174 175document.addEventListener('DOMContentLoaded', () => { 176 loadMedium(); 177 // ð Listen for background updates 178 window.addEventListener("mediumFeedUpdated", (event) => { 179 console.log("Feed updated in background"); 180 loadMedium(); // Re-render with new data 181 }); 182});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.