Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save mhekel/fb974b866a408653b40dc3415236adb4 to your computer and use it in GitHub Desktop.

Select an option

Save mhekel/fb974b866a408653b40dc3415236adb4 to your computer and use it in GitHub Desktop.
Book shelf (reproducing @emmainSTEM project)
<!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