A Pen by Marie Hekel on CodePen.
Created
April 5, 2026 09:09
-
-
Save mhekel/fb974b866a408653b40dc3415236adb4 to your computer and use it in GitHub Desktop.
Book shelf (reproducing @emmainSTEM project)
This file contains hidden or 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"> | |
| <title>Book Shelf</title> | |
| <!-- Fonts: Handwriting Style --> | |
| <link href="https://fonts.googleapis.com/css2?family=Patrick+Hand&display=swap" rel="stylesheet"> | |
| <!-- SortableJS for Drag and Drop --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script> | |
| <style> | |
| :root { | |
| --bg-color: #E8D5D3; | |
| --shelf-wood: #C4A484; | |
| --shelf-shadow: #A68A6D; | |
| --text-color: #4A4A4A; | |
| --accent-dark: #2C2C2C; | |
| /* Pastel Book Colors */ | |
| --c1: #8FB8DE; /* Blue */ | |
| --c2: #F4A6B8; /* Pink */ | |
| --c3: #F4C095; /* Orange */ | |
| --c4: #A8D5BA; /* Green */ | |
| --c5: #C6BAD8; /* Purple */ | |
| --c6: #E0E0E0; /* Grey */ | |
| --c7: #3E4E50; /* Dark Slate */ | |
| } | |
| * { | |
| box-sizing: border-box; | |
| font-family: 'Patrick Hand', cursive; | |
| } | |
| body { | |
| background-color: var(--bg-color); | |
| color: var(--text-color); | |
| margin: 0; | |
| display: flex; | |
| justify-content: center; | |
| min-height: 100vh; | |
| } | |
| .container { | |
| width: 100%; | |
| max-width: 900px; | |
| padding: 20px; | |
| text-align: center; | |
| } | |
| h1 { | |
| font-size: 3.5rem; | |
| margin-bottom: 60px; | |
| margin-top: 40px; | |
| letter-spacing: 2px; | |
| color: #333; | |
| } | |
| /* --- SHELVES --- */ | |
| .bookshelf-container { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 80px; | |
| margin-bottom: 100px; | |
| } | |
| .shelf-row { | |
| display: flex; | |
| align-items: flex-end; | |
| justify-content: center; | |
| position: relative; | |
| } | |
| .shelf-wood { | |
| width: 750px; | |
| height: 260px; /* Height available for books */ | |
| border-bottom: 18px solid var(--shelf-shadow); | |
| background: linear-gradient(to top, rgba(0,0,0,0.05) 0%, transparent 15%); | |
| display: flex; | |
| align-items: flex-end; | |
| justify-content: center; | |
| padding: 0 20px; | |
| border-radius: 0 0 8px 8px; | |
| gap: 5px; /* Space between books */ | |
| } | |
| /* Decorations */ | |
| .decoration { | |
| font-size: 3.5rem; | |
| position: absolute; | |
| bottom: 18px; | |
| z-index: 0; | |
| filter: drop-shadow(2px 2px 2px rgba(0,0,0,0.2)); | |
| } | |
| .pot { left: 0px; } | |
| .girl { right: 10px; bottom: 18px; font-size: 4rem;} | |
| .rocket { left: 10px; } | |
| .potion { right: 20px; } | |
| /* --- BOOKS --- */ | |
| .book { | |
| width: 48px; | |
| border: 2px solid rgba(0,0,0,0.15); | |
| border-radius: 5px; | |
| cursor: grab; | |
| position: relative; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| box-shadow: 3px 0 5px rgba(0,0,0,0.15); | |
| z-index: 10; /* Above decorations */ | |
| } | |
| .book:active { | |
| cursor: grabbing; | |
| } | |
| .book:hover { | |
| transform: translateY(-10px) scale(1.02); | |
| z-index: 20; | |
| } | |
| /* Vertical Spine Text */ | |
| .book-spine-text { | |
| writing-mode: vertical-rl; | |
| text-orientation: mixed; | |
| transform: rotate(180deg); | |
| font-size: 1rem; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| max-height: 90%; | |
| text-transform: uppercase; | |
| letter-spacing: 1.5px; | |
| font-weight: bold; | |
| pointer-events: none; /* Let clicks pass to book */ | |
| } | |
| /* Cosmetic details on spines */ | |
| .book.deco::before { | |
| content: ''; | |
| position: absolute; | |
| top: 15px; bottom: 15px; left: 50%; | |
| width: 24px; border: 2px solid rgba(0,0,0,0.1); | |
| transform: translateX(-50%); | |
| border-radius: 10px; | |
| pointer-events: none; | |
| } | |
| /* --- FLOATING BUTTON --- */ | |
| #add-book-btn { | |
| position: fixed; | |
| bottom: 40px; | |
| right: 40px; | |
| background: var(--accent-dark); | |
| color: white; | |
| border: none; | |
| padding: 15px 30px; | |
| border-radius: 50px; | |
| font-size: 1.2rem; | |
| cursor: pointer; | |
| box-shadow: 0 6px 15px rgba(0,0,0,0.3); | |
| transition: transform 0.2s; | |
| z-index: 50; | |
| } | |
| #add-book-btn:hover { | |
| transform: scale(1.1) rotate(-2deg); | |
| } | |
| /* --- MODALS --- */ | |
| .hidden { display: none !important; } | |
| .modal-overlay { | |
| position: fixed; | |
| top: 0; left: 0; right: 0; bottom: 0; | |
| background: rgba(60, 50, 50, 0.6); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 100; | |
| backdrop-filter: blur(3px); | |
| } | |
| /* Add Book Form */ | |
| .modal-card { | |
| background: #FFF9F2; | |
| padding: 30px 40px; | |
| border-radius: 20px; | |
| width: 450px; | |
| text-align: left; | |
| box-shadow: 0 20px 50px rgba(0,0,0,0.3); | |
| border: 5px solid var(--bg-color); | |
| } | |
| .modal-card h2 { margin-top: 0; font-size: 2rem; text-align: center; } | |
| .form-group { margin-bottom: 18px; } | |
| .form-group label { | |
| display: block; | |
| font-size: 1.1rem; | |
| margin-bottom: 6px; | |
| font-weight: bold; | |
| } | |
| input[type="text"], textarea { | |
| width: 100%; | |
| padding: 10px; | |
| border: 2px solid #ddd; | |
| border-radius: 10px; | |
| background: #fff; | |
| font-size: 1.1rem; | |
| outline: none; | |
| } | |
| input[type="text"]:focus, textarea:focus { border-color: var(--shelf-wood); } | |
| /* Toggle Switch for Shelf */ | |
| .radio-toggle { | |
| display: flex; | |
| background: #eee; | |
| border-radius: 20px; | |
| padding: 4px; | |
| } | |
| .radio-toggle input { display: none; } | |
| .radio-toggle label { | |
| flex: 1; | |
| text-align: center; | |
| padding: 8px; | |
| cursor: pointer; | |
| border-radius: 16px; | |
| margin: 0; | |
| font-size: 1rem; | |
| } | |
| .radio-toggle input:checked + label { | |
| background: var(--accent-dark); | |
| color: white; | |
| } | |
| /* Range Slider */ | |
| .slider-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| } | |
| input[type=range] { | |
| flex-grow: 1; | |
| accent-color: var(--accent-dark); | |
| height: 6px; | |
| } | |
| .form-actions { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-top: 30px; | |
| align-items: center; | |
| } | |
| .btn-submit { | |
| background: var(--accent-dark); | |
| color: white; | |
| border: none; | |
| padding: 10px 25px; | |
| border-radius: 25px; | |
| font-size: 1.2rem; | |
| cursor: pointer; | |
| } | |
| .btn-cancel { | |
| background: transparent; | |
| border: none; | |
| cursor: pointer; | |
| font-size: 1.1rem; | |
| color: #888; | |
| text-decoration: underline; | |
| } | |
| /* --- VIEW MODAL (OPEN BOOK) --- */ | |
| .modal-book-open { | |
| background: #FFF9F0; | |
| width: 750px; | |
| height: 500px; | |
| border-radius: 12px; | |
| display: flex; | |
| box-shadow: 0 20px 60px rgba(0,0,0,0.4); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| /* Center spine line */ | |
| .modal-book-open::after { | |
| content: ''; | |
| position: absolute; | |
| left: 50%; top: 10px; bottom: 10px; | |
| width: 2px; | |
| background: rgba(0,0,0,0.1); | |
| transform: translateX(-50%); | |
| } | |
| .page { | |
| flex: 1; | |
| padding: 40px; | |
| display: flex; | |
| flex-direction: column; | |
| overflow-y: auto; | |
| } | |
| .left-page { | |
| background: linear-gradient(to right, #fffbf5 0%, #f2eadd 100%); | |
| } | |
| .right-page { | |
| background: linear-gradient(to left, #fffbf5 0%, #f2eadd 100%); | |
| font-size: 1.3rem; | |
| } | |
| #view-title { font-size: 2.2rem; margin: 0; line-height: 1.1;} | |
| #view-author { font-size: 1.2rem; color: #666; font-style: italic; margin-top: 5px; } | |
| .view-section { margin-top: 25px; } | |
| .view-section label { font-size: 0.9rem; color: #999; letter-spacing: 1px; display: block; margin-bottom: 5px;} | |
| .pill { background: #e0e0e0; padding: 5px 15px; border-radius: 15px; } | |
| /* Impact Bar Visual */ | |
| .impact-display { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-size: 0.9rem; | |
| } | |
| .bar-bg { | |
| flex: 1; height: 10px; background: #ddd; border-radius: 5px; overflow: hidden; | |
| } | |
| .bar-fill { | |
| height: 100%; background: var(--text-color); width: 0%; | |
| } | |
| .handwritten-note { | |
| font-family: 'Patrick Hand', cursive; | |
| color: #444; | |
| font-size: 1.2rem; | |
| line-height: 1.4; | |
| } | |
| #view-stars { color: #FFD700; font-size: 1.5rem; letter-spacing: 3px; } | |
| .close-text { | |
| position: absolute; | |
| bottom: 20px; right: 30px; | |
| background: none; border: none; | |
| font-size: 1.2rem; color: #888; | |
| cursor: pointer; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>book shelf</h1> | |
| </header> | |
| <div class="bookshelf-container"> | |
| <!-- TOP SHELF --> | |
| <div class="shelf-row"> | |
| <div class="decoration pot">🪴</div> | |
| <div class="shelf-wood" id="shelf-top"> | |
| <!-- Books injected via JS --> | |
| </div> | |
| <div class="decoration girl">👧🏻</div> | |
| </div> | |
| <!-- BOTTOM SHELF --> | |
| <div class="shelf-row"> | |
| <div class="decoration rocket">🚀</div> | |
| <div class="shelf-wood" id="shelf-bottom"> | |
| <!-- Books injected via JS --> | |
| </div> | |
| <div class="decoration potion">⚗️</div> | |
| </div> | |
| </div> | |
| <button id="add-book-btn" onclick="toggleModal('modal-add')">+ ADD BOOK</button> | |
| </div> | |
| <!-- MODAL: ADD BOOK --> | |
| <div id="modal-add" class="modal-overlay hidden"> | |
| <div class="modal-card"> | |
| <h2>ADD A BOOK</h2> | |
| <form id="add-book-form"> | |
| <div class="form-group"> | |
| <label>WHICH SHELF?</label> | |
| <div class="radio-toggle"> | |
| <input type="radio" name="shelf" value="top" id="s-top" checked> | |
| <label for="s-top">TOP SHELF</label> | |
| <input type="radio" name="shelf" value="bottom" id="s-bottom"> | |
| <label for="s-bottom">BOTTOM SHELF</label> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label>TITLE</label> | |
| <input type="text" id="inp-title" required placeholder="Example Title"> | |
| </div> | |
| <div class="form-group"> | |
| <label>AUTHOR</label> | |
| <input type="text" id="inp-author" required placeholder="Author Name"> | |
| </div> | |
| <div class="form-group"> | |
| <label>GENRE</label> | |
| <input type="text" id="inp-genre" placeholder="Fiction, Romance..."> | |
| </div> | |
| <div class="form-group"> | |
| <label>REVIEW / NOTES</label> | |
| <textarea id="inp-review" rows="3" placeholder="What did you think?"></textarea> | |
| </div> | |
| <div class="form-group"> | |
| <label>EMOTIONAL IMPACT</label> | |
| <div class="slider-container"> | |
| <span>chill</span> | |
| <input type="range" id="inp-impact" min="0" max="100" value="50"> | |
| <span>ruined me</span> | |
| </div> | |
| </div> | |
| <div class="form-actions"> | |
| <button type="button" class="btn-cancel" onclick="toggleModal('modal-add')">CANCEL</button> | |
| <button type="submit" class="btn-submit">ADD TO SHELF</button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- MODAL: VIEW BOOK --> | |
| <div id="modal-view" class="modal-overlay hidden"> | |
| <div class="modal-book-open"> | |
| <div class="page left-page"> | |
| <h2 id="view-title">Book Title</h2> | |
| <p id="view-author">by Author Name</p> | |
| <div class="view-section"> | |
| <label>RATING</label> | |
| <div id="view-stars">★★★★★</div> | |
| </div> | |
| <div class="view-section"> | |
| <label>GENRE</label> | |
| <span id="view-genre" class="pill">Fiction</span> | |
| </div> | |
| <div class="view-section"> | |
| <label>EMOTIONAL IMPACT</label> | |
| <div class="impact-display"> | |
| <span>chill</span> | |
| <div class="bar-bg"><div id="view-impact-fill" class="bar-fill"></div></div> | |
| <span>ruined me</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="page right-page"> | |
| <div class="view-section"> | |
| <label>MY THOUGHTS</label> | |
| <p id="view-review" class="handwritten-note">Review text goes here...</p> | |
| </div> | |
| <button class="close-text" onclick="toggleModal('modal-view')">close book</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // 1. CONFIGURATION & STATE | |
| const colors = [ | |
| 'var(--c1)', 'var(--c2)', 'var(--c3)', 'var(--c4)', | |
| 'var(--c5)', 'var(--c7)' | |
| ]; | |
| // Initial Book Data | |
| let books = [ | |
| { id: 1, title: "SAPIENS", author: "Yuval Noah Harari", shelf: "top", color: "var(--c7)", height: 210, genre: "Non-fiction", review: "Completely changed how I look at history.", impact: 60 }, | |
| { id: 2, title: "A LITTLE LIFE", author: "Hanya Yanagihara", shelf: "top", color: "var(--c3)", height: 230, genre: "Fiction", review: "I will never recover from this book.", impact: 100 }, | |
| { id: 3, title: "NORMAL PEOPLE", author: "Sally Rooney", shelf: "top", color: "var(--c7)", height: 195, genre: "Romance", review: "Characters felt so real it hurt.", impact: 75 }, | |
| { id: 4, title: "THE ALCHEMIST", author: "Paulo Coelho", shelf: "bottom", color: "var(--c3)", height: 180, genre: "Fiction", review: "A short, beautiful journey.", impact: 20 }, | |
| { id: 5, title: "ZERO TO ONE", author: "Peter Thiel", shelf: "bottom", color: "var(--c2)", height: 215, genre: "Business", review: "Controversial but insightful.", impact: 40 }, | |
| { id: 6, title: "DUNE", author: "Frank Herbert", shelf: "bottom", color: "var(--c5)", height: 240, genre: "Sci-Fi", review: "The spice must flow.", impact: 50 } | |
| ]; | |
| const topShelf = document.getElementById('shelf-top'); | |
| const bottomShelf = document.getElementById('shelf-bottom'); | |
| // 2. INITIALIZATION | |
| function init() { | |
| renderBooks(); | |
| initSortable(); | |
| } | |
| function initSortable() { | |
| // Initialize Drag and Drop | |
| new Sortable(topShelf, { group: 'shared', animation: 150 }); | |
| new Sortable(bottomShelf, { group: 'shared', animation: 150 }); | |
| } | |
| // 3. RENDER LOGIC | |
| function renderBooks() { | |
| topShelf.innerHTML = ''; | |
| bottomShelf.innerHTML = ''; | |
| books.forEach(book => { | |
| createBookElement(book); | |
| }); | |
| } | |
| function createBookElement(book) { | |
| const bookEl = document.createElement('div'); | |
| bookEl.className = `book ${Math.random() > 0.5 ? 'deco' : ''}`; | |
| bookEl.style.height = `${book.height}px`; | |
| bookEl.style.backgroundColor = book.color; | |
| // Store object reference for click handler | |
| bookEl.onclick = () => openViewModal(book); | |
| const span = document.createElement('span'); | |
| span.className = 'book-spine-text'; | |
| span.innerText = book.title; | |
| // Contrast handling for dark books | |
| if(book.color === 'var(--c7)') { | |
| span.style.color = '#EEE'; | |
| bookEl.style.borderColor = '#222'; | |
| } | |
| bookEl.appendChild(span); | |
| // Append to correct shelf | |
| if(book.shelf === 'top') { | |
| topShelf.appendChild(bookEl); | |
| } else { | |
| bottomShelf.appendChild(bookEl); | |
| } | |
| } | |
| // 4. MODAL LOGIC | |
| function toggleModal(id) { | |
| const el = document.getElementById(id); | |
| if (el) el.classList.toggle('hidden'); | |
| } | |
| // Click outside to close | |
| window.onclick = function(event) { | |
| if (event.target.classList.contains('modal-overlay')) { | |
| event.target.classList.add('hidden'); | |
| } | |
| } | |
| function openViewModal(book) { | |
| document.getElementById('view-title').innerText = book.title; | |
| document.getElementById('view-author').innerText = `by ${book.author}`; | |
| document.getElementById('view-genre').innerText = book.genre; | |
| document.getElementById('view-review').innerText = book.review; | |
| document.getElementById('view-impact-fill').style.width = `${book.impact}%`; | |
| toggleModal('modal-view'); | |
| } | |
| // 5. ADD BOOK LOGIC | |
| const addForm = document.getElementById('add-book-form'); | |
| addForm.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| // Get values | |
| const title = document.getElementById('inp-title').value; | |
| const author = document.getElementById('inp-author').value; | |
| const genre = document.getElementById('inp-genre').value; | |
| const review = document.getElementById('inp-review').value; | |
| const impact = document.getElementById('inp-impact').value; | |
| const shelfChoice = document.querySelector('input[name="shelf"]:checked').value; | |
| // Randomize appearance | |
| const randomColor = colors[Math.floor(Math.random() * colors.length)]; | |
| const randomHeight = Math.floor(Math.random() * (240 - 180 + 1)) + 180; | |
| const newBook = { | |
| id: Date.now(), // simple unique id | |
| title: title, | |
| author: author, | |
| genre: genre, | |
| review: review, | |
| impact: impact, | |
| shelf: shelfChoice, | |
| color: randomColor, | |
| height: randomHeight | |
| }; | |
| // Add to array | |
| books.push(newBook); | |
| // Add to DOM immediately | |
| createBookElement(newBook); | |
| // Reset and Close | |
| addForm.reset(); | |
| toggleModal('modal-add'); | |
| }); | |
| // Run start | |
| init(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment