diff --git a/data/books.json b/data/books.json new file mode 100644 index 0000000..ee6003d --- /dev/null +++ b/data/books.json @@ -0,0 +1,90 @@ +[ + { + "title": "The Great Gatsby", + "author": "F. Scott Fitzgerald", + "year": 1925, + "genre": "Fiction", + "language": "en", + "image": "https://covers.openlibrary.org/b/id/8221256-L.jpg" + }, + { + "title": "To Kill a Mockingbird", + "author": "Harper Lee", + "year": 1960, + "genre": "Fiction", + "language": "en", + "image": "https://covers.openlibrary.org/b/id/10470884-L.jpg" + }, + { + "title": "1984", + "author": "George Orwell", + "year": 1949, + "genre": "Dystopian", + "language": "en", + "image": "https://covers.openlibrary.org/b/id/11141570-L.jpg" + }, + { + "title": "Pride and Prejudice", + "author": "Jane Austen", + "year": 1813, + "genre": "Romance", + "language": "en", + "image": "https://covers.openlibrary.org/b/id/8231996-L.jpg" + }, + { + "title": "Кобзар", + "author": "Тарас Шевченко", + "year": 1840, + "genre": "Poetry", + "language": "ua", + "image": "https://covers.openlibrary.org/b/id/10579826-L.jpg" + }, + { + "title": "Місто", + "author": "Валер'ян Підмогильний", + "year": 1928, + "genre": "Modernism", + "language": "ua", + "image": "https://covers.openlibrary.org/b/id/10579826-L.jpg" + }, + { + "title": "Brave New World", + "author": "Aldous Huxley", + "year": 1932, + "genre": "Dystopian", + "language": "en", + "image": "https://covers.openlibrary.org/b/id/10579790-L.jpg" + }, + { + "title": "The Alchemist", + "author": "Paulo Coelho", + "year": 1988, + "genre": "Adventure", + "language": "en", + "image": "https://covers.openlibrary.org/b/id/8231843-L.jpg" + }, + { + "title": "Les Misérables", + "author": "Victor Hugo", + "year": 1862, + "genre": "Historical", + "language": "en", + "image": "https://covers.openlibrary.org/b/id/8319256-L.jpg" + }, + { + "title": "Захар Беркут", + "author": "Іван Франко", + "year": 1883, + "genre": "Historical", + "language": "ua", + "image": "https://covers.openlibrary.org/b/id/10579830-L.jpg" + }, + { + "title": "The Catcher in the Rye", + "author": "J.D. Salinger", + "year": 1951, + "genre": "Fiction", + "language": "en", + "image": "https://covers.openlibrary.org/b/id/8221256-L.jpg" +} +] \ No newline at end of file diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..855cd80 --- /dev/null +++ b/src/index.html @@ -0,0 +1,41 @@ + + + + + + Online Book Library + + + + + + + +
+

Online Book Library

+
+ + +
+
+ +
+ +
+
+

Featured

+
Sort by:
+
+
+
+
+
+ + + diff --git a/src/main.js b/src/main.js index e69de29..ffd7ebb 100644 --- a/src/main.js +++ b/src/main.js @@ -0,0 +1,211 @@ +let books = []; +let currentSort = null; +let favorites = new Set(); + +function loadFavorites() { + const saved = localStorage.getItem('bookFavorites'); + if (saved) favorites = new Set(JSON.parse(saved)); +} + +function saveFavorites() { + localStorage.setItem('bookFavorites', JSON.stringify([...favorites])); +} +// Add this to your DOMContentLoaded event +document.addEventListener('DOMContentLoaded', async () => { + loadFavorites(); + try { + books = await fetchBooks(); + renderBooks(books); + buildFilters(books); + setupFilterHandlers(); + setupFavoritesButton(); + setupSortHandlers(); + setupFilterToggle(); + } catch (error) { + console.error('Error initializing application:', error); + showError('Failed to load books. Please try again later.'); + } +}); + +function setupFilterToggle() { + const toggle = document.querySelector('.filters-toggle'); + const filters = document.querySelector('.filters'); + if (!toggle || !filters) return; + + const updateDisplay = () => { + const wide = window.innerWidth > 1024; + filters.style.display = wide ? 'flex' : 'none'; + toggle.style.display = wide ? 'none' : 'inline-block'; + toggle.textContent = wide ? '' : 'Filter'; + }; + + updateDisplay(); + window.addEventListener('resize', updateDisplay); + + toggle.addEventListener('click', () => { + if (window.innerWidth > 1024) return; + const visible = filters.style.display === 'flex'; + filters.style.display = visible ? 'none' : 'flex'; + toggle.textContent = visible ? 'Filter' : 'Hide Filters'; + }); +} + +async function fetchBooks() { + const res = await fetch('../data/books.json'); + return res.json(); +} + +function renderBooks(list) { + const container = document.querySelector('.book-grid'); + container.innerHTML = ''; + list.forEach(book => { + const card = document.createElement('div'); + card.className = 'book-card'; + card.innerHTML = ` + ${book.title} +
+

${book.title}

+

${book.author}

+
+ `; + card.addEventListener('click', () => showPopup(book)); + container.appendChild(card); + }); +} + +function buildFilters(data) { + const genres = [...new Set(data.map(b => b.genre))]; + const years = [...new Set(data.map(b => b.year))].sort(); + const languages = [...new Set(data.map(b => b.language))]; + const section = document.querySelector('.filters > div'); + + const checkboxes = (items, prefix) => items + .map(val => ` +
`).join(''); + + section.innerHTML = ` +

Genre:

${checkboxes(genres, 'g')} +

Author:

+

Year:

${checkboxes(years, 'y')} +

Language:

${checkboxes(languages, 'l')} + `; +} + +function setupFilterHandlers() { + document.querySelector('.filters').addEventListener('input', applyFilters); +} + +function applyFilters() { + const q = sel => [...document.querySelectorAll(sel)]; + const genre = q('input[id^="g-"]:checked').map(i => i.value); + const year = q('input[id^="y-"]:checked').map(i => +i.value); + const lang = q('input[id^="l-"]:checked').map(i => i.value); + const author = document.getElementById('author').value.toLowerCase(); + + let result = books.filter(b => + genre.includes(b.genre) && + (year.length === 0 || year.includes(b.year)) && + lang.includes(b.language) && + b.author.toLowerCase().includes(author) + ); + + if (currentSort) { + result.sort((a, b) => + currentSort === 'year' + ? a.year - b.year + : a[currentSort].localeCompare(b[currentSort]) + ); + } + + if (document.getElementById('favorites-btn').innerText === 'Back to All') { + result = result.filter(b => favorites.has(b.title)); + } + + renderBooks(result); +} + +function setupSortHandlers() { + ['sort1', 'sort2'].forEach(id => { + const btn = document.getElementById(id); + if (!btn) return; + btn.dataset.sort = ''; + btn.innerHTML = id === 'sort1' ? 'Sort' : 'Sort by:'; + + btn.addEventListener('click', () => { + const opts = ['year', 'genre']; + const current = btn.dataset.sort || 'year'; + const next = opts[(opts.indexOf(current) + 1) % opts.length]; + currentSort = next; + btn.dataset.sort = next; + document.getElementById('sort1').innerHTML = next.charAt(0).toUpperCase() + next.slice(1); + document.getElementById('sort2').innerHTML = `Sort by: ${next.charAt(0).toUpperCase() + next.slice(1)}`; + applyFilters(); + }); + }); +} + +function showPopup(book) { + const modal = document.createElement('div'); + modal.className = 'popup'; + modal.innerHTML = ` + + `; + document.body.appendChild(modal); + + modal.querySelector('#fav-btn').addEventListener('click', () => { + favorites[favorites.has(book.title) ? 'delete' : 'add'](book.title); + saveFavorites(); + document.body.removeChild(modal); + document.getElementById('favorites-btn').innerText === 'Back to All' + ? renderBooks(books.filter(b => favorites.has(b.title))) + : renderBooks(books); + }); + + modal.querySelector('#close-btn').addEventListener('click', () => { + document.body.removeChild(modal); + }); +} + +function setupFavoritesButton() { + const btn = document.createElement('button'); + btn.id = 'favorites-btn'; + Object.assign(btn.style, { + position: 'fixed', + bottom: '20px', + right: '20px', + padding: '10px 20px', + background: '#f3f4f6', + border: '1px solid #ccc', + borderRadius: '5px', + cursor: 'pointer' + }); + btn.innerText = 'Favorites'; + document.body.appendChild(btn); + + btn.addEventListener('click', () => { + document.getElementById('sort2').innerHTML = 'Sort by:'; + document.getElementById('sort1').innerHTML = 'Sort'; + if (btn.innerText === 'Back to All') { + btn.innerText = 'Favorites'; + renderBooks(books); + resetFilters(); + } else { + btn.innerText = 'Back to All'; + renderBooks(books.filter(b => favorites.has(b.title))); + resetFilters(); + } + }); +} + +function resetFilters() { + document.querySelectorAll('.filters input[type="checkbox"]').forEach(i => (i.checked = true)); + document.getElementById('author').value = ''; +} \ No newline at end of file diff --git a/src/report.html b/src/report.html new file mode 100644 index 0000000..cc922c1 --- /dev/null +++ b/src/report.html @@ -0,0 +1,24 @@ + + + + + Аналітика книг + + + + +
+

Аналітика книг

+ ← Назад до бібліотеки +
+ +
+
+
Завантаження даних...
+
+ + + + + + \ No newline at end of file diff --git a/src/report.js b/src/report.js new file mode 100644 index 0000000..564a9a2 --- /dev/null +++ b/src/report.js @@ -0,0 +1,74 @@ +document.addEventListener('DOMContentLoaded', async () => { + try { + const loadingMessage = document.getElementById('loading-message'); + loadingMessage.textContent = 'Завантаження даних...'; + + const response = await fetch('../data/books.json'); + + if (!response.ok) { + throw new Error(`Помилка завантаження: ${response.status}`); + } + + const books = await response.json(); + + const reportData = books.map(book => ({ + Назва: book.title, + Автор: book.author, + Рік: book.year, + Жанр: book.genre, + Мова: book.language, + Зображення: book.image + })); + + new WebDataRocks({ + container: "#wdr-component", + toolbar: true, + report: { + dataSource: { + data: reportData, + dataSourceType: 'json' + }, + slice: { + rows: [ + { uniqueName: "Назва" }, + { uniqueName: "Автор" }, + { uniqueName: "Рік" }, + { uniqueName: "Жанр" }, + { uniqueName: "Мова" }, + { uniqueName: "Зображення" } + ], + measures: [ + { + uniqueName: "Назва", + aggregation: "count", + caption: "Назва" + } + ] + }, + options: { + grid: { + type: "flat", + showTotals: "off", + showGrandTotals: "off" + } + } + }, + global: { + localization: "https://cdn.webdatarocks.com/loc/uk.json" + } + }); + + loadingMessage.style.display = 'none'; + + } catch (error) { + console.error('Помилка:', error); + const loadingMessage = document.getElementById('loading-message'); + loadingMessage.innerHTML = ` +
+

Помилка при завантаженні аналітики: ${error.message}

+ ← Назад до бібліотеки +
+ `; + loadingMessage.classList.add('error-message'); + } +}); \ No newline at end of file diff --git a/style/style.css b/style/style.css index e69de29..038ff03 100644 --- a/style/style.css +++ b/style/style.css @@ -0,0 +1,300 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: white; + color: #1f2937; + line-height: 1.6; + margin: 0rem +} + +header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 2rem; + background-color: white; + border-top: 0; + border-bottom: 1px solid #e5e7eb; +} + +header h1 { + font-size: 1.8rem; + color: #111827; +} + +button { + background-color: white; + border: 1px solid #d1d5db; + padding: 0.5rem 1rem; + border-radius: 0.375rem; + cursor: pointer; + font-size: 0.95rem; + color: #374151; + margin: 0.5rem 0.2rem; +} + +header button { + display: block; + font-weight: 500; + color: #374151; + padding: 0.5rem 1rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + background-color: white; + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-size: 0.95rem; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.container { + display: flex; + flex-direction: column; + padding: 1rem; +} + +.filters { + display: none; + flex-direction: column; + gap: 1rem; + background-color: #fcfcfc; + padding: 1rem; + border: 1px solid #e5e7eb; + border-radius: 8px; + margin-bottom: 1rem; +} + +.filters h3 { + font-size: 1.25rem; + margin-bottom: 0.5rem; +} + +.filters h4 { + margin-top: 1rem; + font-weight: 600; +} + +.filters input[type="text"] { + padding: 0.4rem; + border: 1px solid #d1d5db; + border-radius: 4px; + margin-top: 0.25rem; + width: 100%; +} + +#sort2 { + display: none; +} + + +.book-section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin: 1rem 1rem; +} + +.book-section-header h3 { + font-size: 1.7rem; + color: #1f2937; +} + +.book-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1rem; +} + +.book-card { + display: flex; + flex-direction: row; + align-items: center; + background-color: white; + border-radius: 8px; + padding: 0.75rem; + gap: 1rem; +} + +.book-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 10px rgba(0,0,0,0.1); +} + +.book-card img { + width: 7rem; + height: auto; + border-radius: 4px; + flex-shrink: 0; +} + +.book-card div { + display: flex; + flex-direction: column; + justify-content: center; + gap: 0.2rem; +} + +.book-card h4 { + font-size: 1rem; + color: #111827; + font-weight: 600; +} + +.book-card p { + font-size: 0.875rem; + margin-top: 2px; +} + +.popup { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.4); + display: flex; + justify-content: center; + align-items: center; + z-index: 999; +} + +.popup-content { + background-color: white; + padding: 2rem; + border-radius: 10px; + width: 300px; + box-shadow: 0 0 10px rgba(0,0,0,0.3); +} + + +@media (min-width: 600px) { + body{ + margin: 1rem; + border: 1px solid #e5e7eb; + border-radius: 15px; + box-shadow: 0 4px 10px rgba(0,0,0,0.1); + } + header { + border-top-left-radius: 15px; + border-top-right-radius: 15px; + } + header button{ + border: none + } + .container{ + border-bottom-left-radius: 15px; + border-bottom-right-radius: 15px; + } + #sort1::after{ + content: '▾'; + font-size: 1rem; + color: #4b5563; + margin-left: 0.2rem; + } + + .book-section-header h3 { + font-size: 2rem; + } + .book-grid { + grid-template-columns: repeat(2, 1fr); + } + + .book-card { + flex-direction: column; + align-items: flex-start; + text-align: left; + } + + .book-card img { + width: 100%; + margin-bottom: 0.5rem; + } + + .book-card div { + align-items: flex-start; + } + + #sort2 { + display: none; + } +} + +@media (min-width: 1024px) { + body{ + margin: 0; + border-radius: 0; + } + header { + border-radius: 0; + border-top:0; + background-color: #fcfcfc; +} +header button{ + background-color: #fcfcfc; +} + + header button:hover { + background-color: #e4e6e9; +} + .book-section-header h3 { + font-size: 1.5rem; + } + .container { + flex-direction: row; + gap: 2rem; + } + + aside { + width: 250px; + } + + .filters-toggle { + display: none; + } + + .filters { + display: flex; + margin-bottom: 0; + } + + #sort1 { + display: none; + } + + #sort2 { + display: block; + font-weight: 500; + color: #374151; + padding: 0.5rem 1rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + background-color: white; + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-size: 0.95rem; + cursor: pointer; +} + +#sort2::after { + content: '▾'; + font-size: 1rem; + color: #4b5563; + margin-left: 1rem; +} + + .book-grid { + grid-template-columns: repeat(4, 1fr); + } + + .book-card { + padding: 0.75rem; + } +}