diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..b2cd33f8 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +**/vendor/** diff --git a/.eslintrc.js b/.eslintrc.js index d39859f1..b65fe988 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,5 +6,85 @@ module.exports = { "es6": true }, - "extends": "eslint:recommended" + "extends": "eslint:recommended", + + "rules": { + // Possible Errors + "no-async-promise-executor": "error", + "no-await-in-loop": "error", + "no-class-assign": "error", + "no-confusing-arrow": "error", + "no-const-assign": "error", + "no-dupe-class-members": "error", + "no-duplicate-imports": "error", + "no-template-curly-in-string": "error", + "no-useless-computed-key": "error", + "no-useless-constructor": "error", + "no-useless-rename": "error", + "require-atomic-updates": "error", + + // Best practices + "strict": "error", + "no-var": "error", + + // Stylistic Issues + "arrow-spacing": "error", + "capitalized-comments": [ + "warn", + "always", + { + "ignoreConsecutiveComments": true + }, + ], + "keyword-spacing": "error", + "lines-around-comment": [ + "error", + { + "beforeBlockComment": true, + "beforeLineComment": true, + "allowBlockStart": true, + "allowClassStart": true, + "allowObjectStart": true, + "allowArrayStart": true, + }, + ], + "no-multiple-empty-lines": [ + "error", + { + "max": 1, + }, + ], + "padded-blocks": [ + "error", + "never", + ], + "padding-line-between-statements": [ + "error", + { + // always before return + "blankLine": "always", + "prev": "*", + "next": "return", + }, + { + // always before block-like expressions + "blankLine": "always", + "prev": "*", + "next": "block-like", + }, + { + // always after variable declaration + "blankLine": "always", + "prev": [ "const", "let", "var" ], + "next": "*", + }, + { + // not necessary between variable declaration + "blankLine": "any", + "prev": [ "const", "let", "var" ], + "next": [ "const", "let", "var" ], + }, + ], + "space-before-blocks": "error", + } }; diff --git a/.github/workflows/lint-frontend.yaml b/.github/workflows/lint-frontend.yaml index 978bbbbe..f370d83b 100644 --- a/.github/workflows/lint-frontend.yaml +++ b/.github/workflows/lint-frontend.yaml @@ -3,12 +3,14 @@ name: Lint Frontend on: push: - branches: [ main, ci ] + branches: [ main, ci, frontend ] paths: - '.github/workflows/**' - 'static/**' + - '.eslintrc' + - '.stylelintrc' pull_request: - branches: [ main, ci ] + branches: [ main, ci, frontend ] jobs: lint: @@ -22,8 +24,10 @@ jobs: - name: Install modules run: yarn + # See .stylelintignore for files that are not linted. - name: Run stylelint - run: yarn stylelint **/static/**/*.css --report-needless-disables --report-invalid-scope-disables + run: yarn stylelint bookwyrm/static/**/*.css --report-needless-disables --report-invalid-scope-disables + # See .eslintignore for files that are not linted. - name: Run ESLint - run: yarn eslint . --ext .js,.jsx,.ts,.tsx + run: yarn eslint bookwyrm/static --ext .js,.jsx,.ts,.tsx diff --git a/.stylelintignore b/.stylelintignore index f456cb22..b2cd33f8 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -1,2 +1 @@ -bookwyrm/static/css/bulma.*.css* -bookwyrm/static/css/icons.css +**/vendor/** diff --git a/README.md b/README.md index dc3af920..6dad4178 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,8 @@ If you edit the CSS or JavaScript, you will need to run Django's `collectstatic` ./bw-dev collectstatic ``` +If you have [installed yarn](https://yarnpkg.com/getting-started/install), you can run `yarn watch:static` to automatically run the previous script every time a change occurs in _bookwyrm/static_ directory. + ### Working with translations and locale files Text in the html files are wrapped in translation tags (`{% trans %}` and `{% blocktrans %}`), and Django generates locale files for all the strings in which you can add translations for the text. You can find existing translations in the `locale/` directory. diff --git a/bookwyrm/static/css/format.css b/bookwyrm/static/css/bookwyrm.css similarity index 73% rename from bookwyrm/static/css/format.css rename to bookwyrm/static/css/bookwyrm.css index a01aff82..e4bca203 100644 --- a/bookwyrm/static/css/format.css +++ b/bookwyrm/static/css/bookwyrm.css @@ -3,7 +3,6 @@ html { scroll-padding-top: 20%; } -/* --- --- */ .image { overflow: hidden; } @@ -25,17 +24,8 @@ html { min-width: 75% !important; } -/* --- "disabled" for non-buttons --- */ -.is-disabled { - background-color: #dbdbdb; - border-color: #dbdbdb; - box-shadow: none; - color: #7a7a7a; - opacity: 0.5; - cursor: not-allowed; -} - -/* --- SHELVING --- */ +/** Shelving + ******************************************************************************/ /** @todo Replace icons with SVG symbols. @see https://www.youtube.com/watch?v=9xXBYcWgCHA */ @@ -45,7 +35,9 @@ html { margin-left: 0.5em; } -/* --- TOGGLES --- */ +/** Toggles + ******************************************************************************/ + .toggle-button[aria-pressed=true], .toggle-button[aria-pressed=true]:hover { background-color: hsl(171, 100%, 41%); @@ -57,12 +49,8 @@ html { display: none; } -.hidden { - display: none !important; -} - -.hidden.transition-y, -.hidden.transition-x { +.transition-x.is-hidden, +.transition-y.is-hidden { display: block !important; visibility: hidden !important; height: 0; @@ -71,16 +59,18 @@ html { padding: 0; } +.transition-x, .transition-y { - transition-property: height, margin-top, margin-bottom, padding-top, padding-bottom; transition-duration: 0.5s; transition-timing-function: ease; } .transition-x { transition-property: width, margin-left, margin-right, padding-left, padding-right; - transition-duration: 0.5s; - transition-timing-function: ease; +} + +.transition-y { + transition-property: height, margin-top, margin-bottom, padding-top, padding-bottom; } @media (prefers-reduced-motion: reduce) { @@ -121,7 +111,9 @@ html { content: '\e9d7'; } -/* --- BOOK COVERS --- */ +/** Book covers + ******************************************************************************/ + .cover-container { height: 250px; width: max-content; @@ -186,7 +178,9 @@ html { padding: 0.1em; } -/* --- AVATAR --- */ +/** Avatars + ******************************************************************************/ + .avatar { vertical-align: middle; display: inline; @@ -202,25 +196,57 @@ html { min-height: 96px; } -/* --- QUOTES --- */ -.quote blockquote { +/** Statuses: Quotes + * + * \e906: icon-quote-open + * \e905: icon-quote-close + * + * The `content` class on the blockquote allows to apply styles to markdown + * generated HTML in the quote: https://bulma.io/documentation/elements/content/ + * + * ```html + *
+ *
+ * User generated quote in markdown… + *
+ * + *

Book Title by Author

+ *
+ * ``` + ******************************************************************************/ + +.quote > blockquote { position: relative; padding-left: 2em; } -.quote blockquote::before, -.quote blockquote::after { +.quote > blockquote::before, +.quote > blockquote::after { font-family: 'icomoon'; position: absolute; } -.quote blockquote::before { +.quote > blockquote::before { content: "\e906"; top: 0; left: 0; } -.quote blockquote::after { +.quote > blockquote::after { content: "\e905"; right: 0; } + +/* States + ******************************************************************************/ + +/* "disabled" for non-buttons */ + +.is-disabled { + background-color: #dbdbdb; + border-color: #dbdbdb; + box-shadow: none; + color: #7a7a7a; + opacity: 0.5; + cursor: not-allowed; +} diff --git a/bookwyrm/static/css/bulma.css.map b/bookwyrm/static/css/vendor/bulma.css.map similarity index 100% rename from bookwyrm/static/css/bulma.css.map rename to bookwyrm/static/css/vendor/bulma.css.map diff --git a/bookwyrm/static/css/bulma.min.css b/bookwyrm/static/css/vendor/bulma.min.css similarity index 100% rename from bookwyrm/static/css/bulma.min.css rename to bookwyrm/static/css/vendor/bulma.min.css diff --git a/bookwyrm/static/css/icons.css b/bookwyrm/static/css/vendor/icons.css similarity index 86% rename from bookwyrm/static/css/icons.css rename to bookwyrm/static/css/vendor/icons.css index 9915ecd1..c78af145 100644 --- a/bookwyrm/static/css/icons.css +++ b/bookwyrm/static/css/vendor/icons.css @@ -1,10 +1,13 @@ + +/** @todo Replace icons with SVG symbols. + @see https://www.youtube.com/watch?v=9xXBYcWgCHA */ @font-face { font-family: 'icomoon'; - src: url('fonts/icomoon.eot?n5x55'); - src: url('fonts/icomoon.eot?n5x55#iefix') format('embedded-opentype'), - url('fonts/icomoon.ttf?n5x55') format('truetype'), - url('fonts/icomoon.woff?n5x55') format('woff'), - url('fonts/icomoon.svg?n5x55#icomoon') format('svg'); + src: url('../fonts/icomoon.eot?n5x55'); + src: url('../fonts/icomoon.eot?n5x55#iefix') format('embedded-opentype'), + url('../fonts/icomoon.ttf?n5x55') format('truetype'), + url('../fonts/icomoon.woff?n5x55') format('woff'), + url('../fonts/icomoon.svg?n5x55#icomoon') format('svg'); font-weight: normal; font-style: normal; font-display: block; diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js new file mode 100644 index 00000000..485daf15 --- /dev/null +++ b/bookwyrm/static/js/bookwyrm.js @@ -0,0 +1,285 @@ +/* exported BookWyrm */ +/* globals TabGroup */ + +let BookWyrm = new class { + constructor() { + this.initOnDOMLoaded(); + this.initReccuringTasks(); + this.initEventListeners(); + } + + initEventListeners() { + document.querySelectorAll('[data-controls]') + .forEach(button => button.addEventListener( + 'click', + this.toggleAction.bind(this)) + ); + + document.querySelectorAll('.interaction') + .forEach(button => button.addEventListener( + 'submit', + this.interact.bind(this)) + ); + + document.querySelectorAll('.hidden-form input') + .forEach(button => button.addEventListener( + 'change', + this.revealForm.bind(this)) + ); + + document.querySelectorAll('[data-back]') + .forEach(button => button.addEventListener( + 'click', + this.back) + ); + } + + /** + * Execute code once the DOM is loaded. + */ + initOnDOMLoaded() { + window.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('.tab-group') + .forEach(tabs => new TabGroup(tabs)); + }); + } + + /** + * Execute recurring tasks. + */ + initReccuringTasks() { + // Polling + document.querySelectorAll('[data-poll]') + .forEach(liveArea => this.polling(liveArea)); + } + + /** + * Go back in browser history. + * + * @param {Event} event + * @return {undefined} + */ + back(event) { + event.preventDefault(); + history.back(); + } + + /** + * Update a counter with recurring requests to the API + * The delay is slightly randomized and increased on each cycle. + * + * @param {Object} counter - DOM node + * @param {int} delay - frequency for polling in ms + * @return {undefined} + */ + polling(counter, delay) { + const bookwyrm = this; + + delay = delay || 10000; + delay += (Math.random() * 1000); + + setTimeout(function() { + fetch('/api/updates/' + counter.dataset.poll) + .then(response => response.json()) + .then(data => bookwyrm.updateCountElement(counter, data)); + + bookwyrm.polling(counter, delay * 1.25); + }, delay, counter); + } + + /** + * Update a counter. + * + * @param {object} counter - DOM node + * @param {object} data - json formatted response from a fetch + * @return {undefined} + */ + updateCountElement(counter, data) { + const currentCount = counter.innerText; + const count = data.count; + + if (count != currentCount) { + this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-hidden', count < 1); + counter.innerText = count; + } + } + + /** + * Toggle form. + * + * @param {Event} event + * @return {undefined} + */ + revealForm(event) { + let trigger = event.currentTarget; + let hidden = trigger.closest('.hidden-form').querySelectorAll('.is-hidden')[0]; + + this.addRemoveClass(hidden, 'is-hidden', !hidden); + } + + /** + * Execute actions on targets based on triggers. + * + * @param {Event} event + * @return {undefined} + */ + toggleAction(event) { + let trigger = event.currentTarget; + let pressed = trigger.getAttribute('aria-pressed') === 'false'; + let targetId = trigger.dataset.controls; + + // Toggle pressed status on all triggers controlling the same target. + document.querySelectorAll('[data-controls="' + targetId + '"]') + .forEach(otherTrigger => otherTrigger.setAttribute( + 'aria-pressed', + otherTrigger.getAttribute('aria-pressed') === 'false' + )); + + // @todo Find a better way to handle the exception. + if (targetId && ! trigger.classList.contains('pulldown-menu')) { + let target = document.getElementById(targetId); + + this.addRemoveClass(target, 'is-hidden', !pressed); + this.addRemoveClass(target, 'is-active', pressed); + } + + // Show/hide pulldown-menus. + if (trigger.classList.contains('pulldown-menu')) { + this.toggleMenu(trigger, targetId); + } + + // Show/hide container. + let container = document.getElementById('hide-' + targetId); + + if (container) { + this.toggleContainer(container, pressed); + } + + // Check checkbox, if appropriate. + let checkbox = trigger.dataset.controlsCheckbox; + + if (checkbox) { + this.toggleCheckbox(checkbox, pressed); + } + + // Set focus, if appropriate. + let focus = trigger.dataset.focusTarget; + + if (focus) { + this.toggleFocus(focus); + } + } + + /** + * Show or hide menus. + * + * @param {Event} event + * @return {undefined} + */ + toggleMenu(trigger, targetId) { + let expanded = trigger.getAttribute('aria-expanded') == 'false'; + + trigger.setAttribute('aria-expanded', expanded); + + if (targetId) { + let target = document.getElementById(targetId); + + this.addRemoveClass(target, 'is-active', expanded); + } + } + + /** + * Show or hide generic containers. + * + * @param {object} container - DOM node + * @param {boolean} pressed - Is the trigger pressed? + * @return {undefined} + */ + toggleContainer(container, pressed) { + this.addRemoveClass(container, 'is-hidden', pressed); + } + + /** + * Check or uncheck a checbox. + * + * @param {object} checkbox - DOM node + * @param {boolean} pressed - Is the trigger pressed? + * @return {undefined} + */ + toggleCheckbox(checkbox, pressed) { + document.getElementById(checkbox).checked = !!pressed; + } + + /** + * Give the focus to an element. + * Only move the focus based on user interactions. + * + * @param {string} nodeId - ID of the DOM node to focus (button, link…) + * @return {undefined} + */ + toggleFocus(nodeId) { + let node = document.getElementById(nodeId); + + node.focus(); + + setTimeout(function() { + node.selectionStart = node.selectionEnd = 10000; + }, 0); + } + + /** + * Make a request and update the UI accordingly. + * This function is used for boosts, favourites, follows and unfollows. + * + * @param {Event} event + * @return {undefined} + */ + interact(event) { + event.preventDefault(); + + const bookwyrm = this; + const form = event.currentTarget; + const relatedforms = document.querySelectorAll(`.${form.dataset.id}`); + + // Toggle class on all related forms. + relatedforms.forEach(relatedForm => bookwyrm.addRemoveClass( + relatedForm, + 'is-hidden', + relatedForm.className.indexOf('is-hidden') == -1 + )); + + this.ajaxPost(form).catch(error => { + // @todo Display a notification in the UI instead. + console.warn('Request failed:', error); + }); + } + + /** + * Submit a form using POST. + * + * @param {object} form - Form to be submitted + * @return {Promise} + */ + ajaxPost(form) { + return fetch(form.action, { + method : "POST", + body: new FormData(form) + }); + } + + /** + * Add or remove a class based on a boolean condition. + * + * @param {object} node - DOM node to change class on + * @param {string} classname - Name of the class + * @param {boolean} add - Add? + * @return {undefined} + */ + addRemoveClass(node, classname, add) { + if (add) { + node.classList.add(classname); + } else { + node.classList.remove(classname); + } + } +} diff --git a/bookwyrm/static/js/check_all.js b/bookwyrm/static/js/check_all.js index 07d30a68..fd29f2cd 100644 --- a/bookwyrm/static/js/check_all.js +++ b/bookwyrm/static/js/check_all.js @@ -1,17 +1,34 @@ -/* exported toggleAllCheckboxes */ -/** - * Toggle all descendant checkboxes of a target. - * - * Use `data-target="ID_OF_TARGET"` on the node being listened to. - * - * @param {Event} event - change Event - * @return {undefined} - */ -function toggleAllCheckboxes(event) { - const mainCheckbox = event.target; +(function() { + 'use strict'; + + /** + * Toggle all descendant checkboxes of a target. + * + * Use `data-target="ID_OF_TARGET"` on the node on which the event is listened + * to (checkbox, button, link…), where_ID_OF_TARGET_ should be the ID of an + * ancestor for the checkboxes. + * + * @example + * + * @param {Event} event + * @return {undefined} + */ + function toggleAllCheckboxes(event) { + const mainCheckbox = event.target; + + document + .querySelectorAll(`#${mainCheckbox.dataset.target} [type="checkbox"]`) + .forEach(checkbox => checkbox.checked = mainCheckbox.checked); + } document - .querySelectorAll(`#${mainCheckbox.dataset.target} [type="checkbox"]`) - .forEach(checkbox => {checkbox.checked = mainCheckbox.checked;}); -} + .querySelectorAll('[data-action="toggle-all"]') + .forEach(input => { + input.addEventListener('change', toggleAllCheckboxes); + }); +})(); diff --git a/bookwyrm/static/js/localstorage.js b/bookwyrm/static/js/localstorage.js index aa79ee30..05955779 100644 --- a/bookwyrm/static/js/localstorage.js +++ b/bookwyrm/static/js/localstorage.js @@ -1,20 +1,43 @@ -/* exported updateDisplay */ -/* globals addRemoveClass */ +/* exported LocalStorageTools */ +/* globals BookWyrm */ -// set javascript listeners -function updateDisplay(e) { - // used in set reading goal - var key = e.target.getAttribute('data-id'); - var value = e.target.getAttribute('data-value'); - window.localStorage.setItem(key, value); +let LocalStorageTools = new class { + constructor() { + document.querySelectorAll('[data-hide]') + .forEach(t => this.setDisplay(t)); - document.querySelectorAll('[data-hide="' + key + '"]') - .forEach(t => setDisplay(t)); -} - -function setDisplay(el) { - // used in set reading goal - var key = el.getAttribute('data-hide'); - var value = window.localStorage.getItem(key); - addRemoveClass(el, 'hidden', value); + document.querySelectorAll('.set-display') + .forEach(t => t.addEventListener('click', this.updateDisplay.bind(this))); + } + + /** + * Update localStorage, then display content based on keys in localStorage. + * + * @param {Event} event + * @return {undefined} + */ + updateDisplay(event) { + // used in set reading goal + let key = event.target.dataset.id; + let value = event.target.dataset.value; + + window.localStorage.setItem(key, value); + + document.querySelectorAll('[data-hide="' + key + '"]') + .forEach(node => this.setDisplay(node)); + } + + /** + * Toggle display of a DOM node based on its value in the localStorage. + * + * @param {object} node - DOM node to toggle. + * @return {undefined} + */ + setDisplay(node) { + // used in set reading goal + let key = node.dataset.hide; + let value = window.localStorage.getItem(key); + + BookWyrm.addRemoveClass(node, 'is-hidden', value); + } } diff --git a/bookwyrm/static/js/shared.js b/bookwyrm/static/js/shared.js deleted file mode 100644 index 7a198619..00000000 --- a/bookwyrm/static/js/shared.js +++ /dev/null @@ -1,169 +0,0 @@ -/* globals setDisplay TabGroup toggleAllCheckboxes updateDisplay */ - -// set up javascript listeners -window.onload = function() { - // buttons that display or hide content - document.querySelectorAll('[data-controls]') - .forEach(t => t.onclick = toggleAction); - - // javascript interactions (boost/fav) - Array.from(document.getElementsByClassName('interaction')) - .forEach(t => t.onsubmit = interact); - - // handle aria settings on menus - Array.from(document.getElementsByClassName('pulldown-menu')) - .forEach(t => t.onclick = toggleMenu); - - // hidden submit button in a form - document.querySelectorAll('.hidden-form input') - .forEach(t => t.onchange = revealForm); - - // polling - document.querySelectorAll('[data-poll]') - .forEach(el => polling(el)); - - // browser back behavior - document.querySelectorAll('[data-back]') - .forEach(t => t.onclick = back); - - Array.from(document.getElementsByClassName('tab-group')) - .forEach(t => new TabGroup(t)); - - // display based on localstorage vars - document.querySelectorAll('[data-hide]') - .forEach(t => setDisplay(t)); - - // update localstorage - Array.from(document.getElementsByClassName('set-display')) - .forEach(t => t.onclick = updateDisplay); - - // Toggle all checkboxes. - document - .querySelectorAll('[data-action="toggle-all"]') - .forEach(input => { - input.addEventListener('change', toggleAllCheckboxes); - }); -}; - -function back(e) { - e.preventDefault(); - history.back(); -} - -function polling(el, delay) { - delay = delay || 10000; - delay += (Math.random() * 1000); - setTimeout(function() { - fetch('/api/updates/' + el.getAttribute('data-poll')) - .then(response => response.json()) - .then(data => updateCountElement(el, data)); - polling(el, delay * 1.25); - }, delay, el); -} - -function updateCountElement(el, data) { - const currentCount = el.innerText; - const count = data.count; - if (count != currentCount) { - addRemoveClass(el.closest('[data-poll-wrapper]'), 'hidden', count < 1); - el.innerText = count; - } -} - - -function revealForm(e) { - var hidden = e.currentTarget.closest('.hidden-form').getElementsByClassName('hidden')[0]; - if (hidden) { - removeClass(hidden, 'hidden'); - } -} - - -function toggleAction(e) { - var el = e.currentTarget; - var pressed = el.getAttribute('aria-pressed') == 'false'; - - var targetId = el.getAttribute('data-controls'); - document.querySelectorAll('[data-controls="' + targetId + '"]') - .forEach(t => t.setAttribute('aria-pressed', (t.getAttribute('aria-pressed') == 'false'))); - - if (targetId) { - var target = document.getElementById(targetId); - addRemoveClass(target, 'hidden', !pressed); - addRemoveClass(target, 'is-active', pressed); - } - - // show/hide container - var container = document.getElementById('hide-' + targetId); - if (container) { - addRemoveClass(container, 'hidden', pressed); - } - - // set checkbox, if appropriate - var checkbox = el.getAttribute('data-controls-checkbox'); - if (checkbox) { - document.getElementById(checkbox).checked = !!pressed; - } - - // set focus, if appropriate - var focus = el.getAttribute('data-focus-target'); - if (focus) { - var focusEl = document.getElementById(focus); - focusEl.focus(); - setTimeout(function(){ focusEl.selectionStart = focusEl.selectionEnd = 10000; }, 0); - } -} - -function interact(e) { - e.preventDefault(); - ajaxPost(e.target); - var identifier = e.target.getAttribute('data-id'); - Array.from(document.getElementsByClassName(identifier)) - .forEach(t => addRemoveClass(t, 'hidden', t.className.indexOf('hidden') == -1)); -} - -function toggleMenu(e) { - var el = e.currentTarget; - var expanded = el.getAttribute('aria-expanded') == 'false'; - el.setAttribute('aria-expanded', expanded); - var targetId = el.getAttribute('data-controls'); - if (targetId) { - var target = document.getElementById(targetId); - addRemoveClass(target, 'is-active', expanded); - } -} - -function ajaxPost(form) { - fetch(form.action, { - method : "POST", - body: new FormData(form) - }); -} - -function addRemoveClass(el, classname, bool) { - if (bool) { - addClass(el, classname); - } else { - removeClass(el, classname); - } -} - -function addClass(el, classname) { - var classes = el.className.split(' '); - if (classes.indexOf(classname) > -1) { - return; - } - el.className = classes.concat(classname).join(' '); -} - -function removeClass(el, className) { - var classes = []; - if (el.className) { - classes = el.className.split(' '); - } - const idx = classes.indexOf(className); - if (idx > -1) { - classes.splice(idx, 1); - } - el.className = classes.join(' '); -} diff --git a/bookwyrm/static/js/tabs.js b/bookwyrm/static/js/vendor/tabs.js similarity index 100% rename from bookwyrm/static/js/tabs.js rename to bookwyrm/static/js/vendor/tabs.js diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index 4d978d7e..d263e0e0 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -129,7 +129,7 @@ {% trans 'Add Description' as button_text %} {% include 'snippets/toggle/open_button.html' with text=button_text controls_text="add-description" controls_uid=book.id focus="id_description" hide_active=True id="hide-description" %} -