timconspicuous.neocities.org

Better indexing, About and Friends tabs

+192 -229
+62 -7
src/index.page.tsx
··· 44 44 "Powered by [Lume](https://lume.land), [source code on Tangled](https://tangled.sh/@timtinkers.online/neocities)", 45 45 ); 46 46 47 - // Layout to use for this page 47 + // Define the cards that have content - this drives the navigation 48 + export const cardContent = [ 49 + { 50 + cardIndex: 0, 51 + slug: "linktree", 52 + component: "Linktree", 53 + props: { links }, 54 + }, 55 + { 56 + cardIndex: 1, 57 + slug: "progress", 58 + component: "ReadingProgress", 59 + props: {}, 60 + }, 61 + { 62 + cardIndex: 6, 63 + slug: "friends", 64 + component: "Friends", 65 + props: {}, 66 + }, 67 + { 68 + cardIndex: 22, 69 + slug: "about", 70 + component: null, 71 + props: {}, 72 + }, 73 + ]; 74 + 75 + // Make cardContent available to the client-side script 48 76 export const layout = "layout.tsx"; 49 77 50 78 export default ({ comp }: Lume.Data) => { 51 79 return ( 52 80 <div id="tarot-content"> 53 - <div id="card-0" className="card-content"> 54 - <comp.Linktree links={links} /> 55 - </div> 56 - <div id="card-1" className="card-content"> 57 - <comp.ReadingProgress /> 58 - </div> 81 + <script 82 + type="application/json" 83 + id="card-content-data" 84 + dangerouslySetInnerHTML={{ 85 + __html: JSON.stringify(cardContent), 86 + }} 87 + /> 88 + 89 + {cardContent.map((card) => ( 90 + <div 91 + key={card.cardIndex} 92 + id={`card-${card.cardIndex}`} 93 + className="card-content" 94 + > 95 + {card.component === "Linktree" && ( 96 + <comp.Linktree {...card.props} /> 97 + )} 98 + {card.component === "ReadingProgress" && ( 99 + <comp.ReadingProgress {...card.props} /> 100 + )} 101 + {card.component === "Friends" && <p> 102 + Check out my friends on Neocities:<br></br> 103 + <a href="https://emelee.neocities.org/">emelee</a><br></br> 104 + <a href="https://looueez.neocities.org/">looueez</a><br></br> 105 + <a href="https://halfbloom.neocities.org/">halfbloom</a> 106 + </p>} 107 + {card.component === null && <p> 108 + Made with <a href="https://deno.com/">Deno</a> & <a href="https://lume.land/">Lume</a>.<br></br> 109 + Source code on <a href="https://tangled.sh/@timtinkers.online/neocities">Tangled</a>.<br></br> 110 + Major Arcana art by <a href="https://jcanabal.itch.io/major-arcana-pixel-art-free">jcanabal</a>. 111 + </p>} 112 + </div> 113 + ))} 59 114 </div> 60 115 ); 61 116 };
-178
src/scripts/reading-progress.js
··· 1 - class ReadingProgressWidget { 2 - constructor(containerId) { 3 - const element = document.getElementById(containerId); 4 - if (!element) { 5 - throw new Error(`Element with id "${containerId}" not found`); 6 - } 7 - this.container = element; 8 - this.currentBook = null; 9 - this.init(); 10 - } 11 - 12 - async init() { 13 - this.showLoading(); 14 - try { 15 - const progress = await this.fetchReadingProgress(); 16 - const metadata = await this.fetchMetadata(progress.isbn13); 17 - 18 - this.currentBook = { ...progress, ...metadata }; 19 - this.render(); 20 - } catch (error) { 21 - this.showError( 22 - error instanceof Error ? error.message : "Unknown error", 23 - ); 24 - } 25 - } 26 - 27 - async fetchReadingProgress() { 28 - const response = await fetch( 29 - "https://pds.timtinkers.online/xrpc/com.atproto.repo.listRecords?repo=did%3Aplc%3Ao6xucog6fghiyrvp7pyqxcs3&collection=social.popfeed.feed.listItem", 30 - ); 31 - 32 - if (!response.ok) { 33 - throw new Error(`API request failed: ${response.status}`); 34 - } 35 - 36 - const data = await response.json(); 37 - 38 - // Filter for entries with bookProgress field 39 - const booksWithProgress = data.records.filter( 40 - (record) => 41 - record.value.bookProgress && 42 - record.value.bookProgress.updatedAt, 43 - ); 44 - 45 - if (booksWithProgress.length === 0) { 46 - this.currentBook = null; 47 - return; 48 - } 49 - 50 - // Find the most recently updated book 51 - const mostRecent = booksWithProgress.reduce((latest, current) => { 52 - const latestDate = new Date(latest.value.updatedAt); 53 - const currentDate = new Date(current.value.updatedAt); 54 - return currentDate > latestDate ? current : latest; 55 - }); 56 - 57 - return { 58 - isbn13: mostRecent.value.identifiers.isbn13, 59 - progress: mostRecent.value.bookProgress.percent, 60 - updatedAt: mostRecent.value.bookProgress.updatedAt, 61 - totalPages: mostRecent.value.bookProgress.totalPages, 62 - currentPage: mostRecent.value.bookProgress.currentPage, 63 - }; 64 - } 65 - 66 - async fetchMetadata(isbn13) { 67 - const response = await fetch( 68 - `https://openlibrary.org/api/books?bibkeys=ISBN:${isbn13}&format=json&jscmd=data`, 69 - ); 70 - 71 - if (!response.ok) { 72 - throw new Error(`API request failed: ${response.status}`); 73 - } 74 - 75 - const data = await response.json(); 76 - const metadata = Object.values(data)[0]; 77 - 78 - return { 79 - title: metadata.title, 80 - author: metadata.authors[0]?.name, 81 - coverUrl: metadata.cover?.medium, 82 - }; 83 - } 84 - 85 - showLoading() { 86 - this.container.innerHTML = ` 87 - <div class="reading-progress-container reading-progress-loading"> 88 - <div class="progress-skeleton"> 89 - <div class="skeleton-text"></div> 90 - <div class="skeleton-bar"></div> 91 - </div> 92 - </div> 93 - `; 94 - } 95 - 96 - showError(message) { 97 - this.container.innerHTML = ` 98 - <div class="reading-progress-container reading-progress-error"> 99 - <p>📚 Unable to load current reading progress</p> 100 - <small>${message}</small> 101 - </div> 102 - `; 103 - } 104 - 105 - formatDate(dateString) { 106 - return new Date(dateString).toLocaleDateString("en-US", { 107 - month: "short", 108 - day: "numeric", 109 - year: "numeric", 110 - }); 111 - } 112 - 113 - render() { 114 - if (!this.currentBook) { 115 - this.container.innerHTML = ` 116 - <div class="reading-progress-container reading-progress-empty"> 117 - <p>📚 No books currently in progress</p> 118 - </div> 119 - `; 120 - return; 121 - } 122 - 123 - const coverImage = this.currentBook.coverUrl 124 - ? `<img src="${this.currentBook.coverUrl}" alt="Book cover for ${ 125 - this.escapeHtml(this.currentBook.title) 126 - }" class="book-cover" />` 127 - : '<div class="book-cover-placeholder">📖</div>'; 128 - 129 - this.container.innerHTML = ` 130 - <div class="reading-progress-container"> 131 - <div class="reading-progress-header"> 132 - <span>📚</span> Currently Reading 133 - </div> 134 - <div class="book-info"> 135 - ${coverImage} 136 - <div class="book-details"> 137 - <div class="book-title"> 138 - ${this.escapeHtml(this.currentBook.title)} 139 - </div> 140 - <div class="book-author"> 141 - by ${this.escapeHtml(this.currentBook.author)} 142 - </div> 143 - <div class="book-meta"> 144 - <span class="progress-badge">In progress</span> 145 - <span class="last-updated"> 146 - Updated ${this.formatDate(this.currentBook.updatedAt)} 147 - </span> 148 - </div> 149 - </div> 150 - </div> 151 - <div class="progress-container"> 152 - <div class="progress-bar"> 153 - <div class="progress-fill" style="width: ${this.currentBook.progress}%"></div> 154 - </div> 155 - <div class="progress-details"> 156 - <span class="progress-percent">${this.currentBook.progress}%</span> 157 - <span class="progress-pages">${this.currentBook.currentPage} / ${this.currentBook.totalPages} pages</span> 158 - </div> 159 - </div> 160 - </div> 161 - `; 162 - } 163 - escapeHtml(text) { 164 - const div = document.createElement("div"); 165 - div.textContent = text; 166 - return div.innerHTML; 167 - } 168 - } 169 - 170 - // Auto-initialize when DOM is loaded 171 - document.addEventListener("DOMContentLoaded", () => { 172 - const widgets = document.querySelectorAll("[data-reading-progress]"); 173 - widgets.forEach((widget) => { 174 - if (widget.id) { 175 - new ReadingProgressWidget(widget.id); 176 - } 177 - }); 178 - });
+117 -44
src/scripts/tarot.js
··· 1 1 class Tarot { 2 2 constructor() { 3 - this.currentCard = 0; 4 - this.totalCards = 22; // 0-21 for Major Arcana 3 + this.currentCardIndex = 0; 5 4 this.isTransitioning = false; 6 5 this.isFirstLoad = true; 7 6 8 - // Major Arcana card names and roman numerals 9 - this.cardData = [ 7 + // Full Major Arcana + back card data 8 + this.allCardData = [ 10 9 { id: "0", name: "The Fool" }, 11 10 { id: "I", name: "The Magician" }, 12 11 { id: "II", name: "The High Priestess" }, ··· 29 28 { id: "XIX", name: "The Sun" }, 30 29 { id: "XX", name: "Judgement" }, 31 30 { id: "XXI", name: "The World" }, 31 + 32 + { id: "back", name: "Return" }, 32 33 ]; 33 34 35 + // Available cards (loaded from page data) 36 + this.availableCards = []; 37 + this.loadAvailableCards(); 38 + 34 39 this.init(); 35 40 } 36 41 42 + loadAvailableCards() { 43 + const cardContentData = document.getElementById("card-content-data"); 44 + if (!cardContentData) { 45 + console.error("Card content data not found"); 46 + return; 47 + } 48 + 49 + try { 50 + const cardContent = JSON.parse(cardContentData.textContent); 51 + 52 + // Map card content to available cards with full data 53 + this.availableCards = cardContent.map((card) => { 54 + const tarotCard = this.allCardData[card.cardIndex] || { 55 + id: card.cardIndex.toString(), 56 + name: "Unknown Card", 57 + }; 58 + 59 + return { 60 + ...tarotCard, 61 + cardIndex: card.cardIndex, 62 + slug: card.slug, 63 + component: card.component, 64 + }; 65 + }); 66 + 67 + console.log("Available cards:", this.availableCards); 68 + } catch (error) { 69 + console.error("Error parsing card content data:", error); 70 + } 71 + } 72 + 37 73 init() { 38 74 this.setupEventListeners(); 39 75 this.setupTouchEvents(); ··· 88 124 () => this.nextCard(), 89 125 ); 90 126 127 + // Make card indicator clickable to return to first card (card 0) 128 + document.getElementById("card-indicator")?.addEventListener( 129 + "click", 130 + () => this.goToFirstCard(), 131 + ); 132 + 91 133 // Keyboard navigation 92 134 document.addEventListener("keydown", (e) => { 93 135 if (e.key === "ArrowLeft") { ··· 96 138 } else if (e.key === "ArrowRight") { 97 139 e.preventDefault(); 98 140 this.nextCard(); 141 + } else if (e.key === "Home") { 142 + e.preventDefault(); 143 + this.goToFirstCard(); 99 144 } 100 145 }); 101 146 ··· 138 183 loadFromHash() { 139 184 const hash = globalThis.location.hash.slice(1); 140 185 if (!hash) { 141 - this.currentCard = 0; 186 + this.currentCardIndex = 0; 142 187 this.updateHash(); 143 188 return; 144 189 } 145 190 146 - // Find card index 147 - const cardIndex = this.cardData.findIndex((card) => card.id === hash); 191 + // Try to find by card ID first, then by slug 192 + let foundIndex = this.availableCards.findIndex((card) => 193 + card.id === hash || card.slug === hash 194 + ); 148 195 149 - if (cardIndex !== -1) { 150 - this.currentCard = cardIndex; 196 + // If not found, try parsing as card index 197 + if (foundIndex === -1) { 198 + const cardIndex = parseInt(hash); 199 + if (!isNaN(cardIndex)) { 200 + foundIndex = this.availableCards.findIndex((card) => 201 + card.cardIndex === cardIndex 202 + ); 203 + } 204 + } 205 + 206 + if (foundIndex !== -1) { 207 + this.currentCardIndex = foundIndex; 151 208 } else { 152 - // Try parsing as number 153 - const num = parseInt(hash); 154 - if (!isNaN(num) && num >= 0 && num < this.totalCards) { 155 - this.currentCard = num; 156 - } 209 + // Default to first card if hash doesn't match anything 210 + this.currentCardIndex = 0; 157 211 } 158 212 159 213 this.updateImage(); ··· 161 215 } 162 216 163 217 updateHash() { 164 - const card = this.cardData[this.currentCard]; 165 - globalThis.location.hash = card.id; 218 + const currentCard = this.availableCards[this.currentCardIndex]; 219 + if (currentCard) { 220 + // Prefer slug over card ID for URL 221 + globalThis.location.hash = currentCard.id || currentCard.slug; 222 + } 223 + } 224 + 225 + async goToFirstCard() { 226 + if (this.isTransitioning) return; 227 + 228 + // Find the first card (card index 0) 229 + const firstCardIndex = this.availableCards.findIndex((card) => 230 + card.cardIndex === 0 231 + ); 232 + if (firstCardIndex !== -1 && firstCardIndex !== this.currentCardIndex) { 233 + const direction = firstCardIndex < this.currentCardIndex 234 + ? "right" 235 + : "left"; 236 + this.currentCardIndex = firstCardIndex; 237 + await this.transitionToCard(direction); 238 + } 166 239 } 167 240 168 241 async previousCard() { 169 242 if (this.isTransitioning) return; 170 243 171 - this.currentCard = this.currentCard > 0 172 - ? this.currentCard - 1 173 - : this.totalCards - 1; 244 + this.currentCardIndex = this.currentCardIndex > 0 245 + ? this.currentCardIndex - 1 246 + : this.availableCards.length - 1; 174 247 await this.transitionToCard("right"); 175 248 } 176 249 177 250 async nextCard() { 178 251 if (this.isTransitioning) return; 179 252 180 - this.currentCard = this.currentCard < this.totalCards - 1 181 - ? this.currentCard + 1 182 - : 0; 253 + this.currentCardIndex = 254 + this.currentCardIndex < this.availableCards.length - 1 255 + ? this.currentCardIndex + 1 256 + : 0; 183 257 await this.transitionToCard("left"); 184 258 } 185 259 ··· 270 344 271 345 updateImage() { 272 346 const cardImage = document.getElementById("card-image"); 273 - 274 347 if (!cardImage) return; 275 348 349 + const currentCard = this.availableCards[this.currentCardIndex]; 350 + if (!currentCard) return; 351 + 352 + // Handle special case for back card 353 + if (currentCard.cardIndex === 22) { 354 + cardImage.src = "/images/tarot/back.png"; 355 + cardImage.alt = "Card Back"; 356 + return; 357 + } 358 + 276 359 // Set image source for current card (using PNG extension for pixel art) 277 - const cardName = this.cardData[this.currentCard].name.toLowerCase() 278 - .replace(/\s+/g, "-"); 279 - const newSrc = `/images/tarot/${this.currentCard}-${cardName}.png`; 360 + const cardName = currentCard.name.toLowerCase().replace(/\s+/g, "-"); 361 + const newSrc = `/images/tarot/${currentCard.cardIndex}-${cardName}.png`; 280 362 281 363 // Update image source and alt text 282 364 cardImage.src = newSrc; 283 - cardImage.alt = this.cardData[this.currentCard].name; 365 + cardImage.alt = currentCard.name; 284 366 } 285 367 286 368 updateCardIndicator() { 287 369 const indicator = document.getElementById("current-card"); 288 370 if (indicator) { 289 - indicator.textContent = this.cardData[this.currentCard].id; 371 + const currentCard = this.availableCards[this.currentCardIndex]; 372 + if (currentCard) { 373 + indicator.textContent = currentCard.id; 374 + } 290 375 } 291 376 } 292 377 ··· 295 380 document.querySelectorAll(".card-content").forEach((card) => { 296 381 card.classList.remove("active"); 297 382 }); 298 - 299 - const currentCard = this.cardData[this.currentCard]; 383 + 384 + const currentCard = this.availableCards[this.currentCardIndex]; 385 + if (!currentCard) return; 300 386 301 387 // Update the content title 302 388 const contentTitle = document.getElementById("content-title"); ··· 306 392 307 393 // Show the current card's content 308 394 const currentCardContent = document.getElementById( 309 - `card-${this.currentCard}`, 395 + `card-${currentCard.cardIndex}`, 310 396 ); 311 397 312 398 if (currentCardContent) { 313 399 currentCardContent.classList.add("active"); 314 - } else { 315 - // Fallback content if card doesn't exist yet 316 - const contentWrapper = document.getElementById("content-wrapper"); 317 - if (contentWrapper) { 318 - // Create a new content element with the proper ID 319 - const newContent = document.createElement("div"); 320 - newContent.id = `card-${this.currentCard}`; 321 - newContent.className = "card-content active"; 322 - newContent.innerHTML = ` 323 - <p>Coming soon...</p> 324 - `; 325 - contentWrapper.appendChild(newContent); 326 - } 327 400 } 328 401 } 329 402
+13
src/styles.css
··· 201 201 font-weight: bold; 202 202 min-width: 60px; 203 203 text-align: center; 204 + cursor: pointer; 205 + transition: all 0.3s ease; 206 + user-select: none; 207 + } 208 + 209 + .card-indicator:hover { 210 + background: rgba(48, 37, 75, 0.2); 211 + border-color: rgba(75, 57, 118, 0.6); 212 + transform: scale(1.05); 213 + } 214 + 215 + .card-indicator:active { 216 + transform: scale(0.95); 204 217 } 205 218 206 219 /* Responsive design */