diff --git a/.env.dev.example b/.env.dev.example index 9a4366e0..22e12de1 100644 --- a/.env.dev.example +++ b/.env.dev.example @@ -69,7 +69,7 @@ AWS_SECRET_ACCESS_KEY= # or use_dominant_color_light / use_dominant_color_dark PREVIEW_BG_COLOR=use_dominant_color_light # Change to #FFF if you use use_dominant_color_dark -PREVIEW_TEXT_COLOR="#363636" +PREVIEW_TEXT_COLOR=#363636 PREVIEW_IMG_WIDTH=1200 PREVIEW_IMG_HEIGHT=630 -PREVIEW_DEFAULT_COVER_COLOR="#002549" +PREVIEW_DEFAULT_COVER_COLOR=#002549 diff --git a/.env.prod.example b/.env.prod.example index 56f52a28..2e0ced5e 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -69,7 +69,7 @@ AWS_SECRET_ACCESS_KEY= # or use_dominant_color_light / use_dominant_color_dark PREVIEW_BG_COLOR=use_dominant_color_light # Change to #FFF if you use use_dominant_color_dark -PREVIEW_TEXT_COLOR="#363636" +PREVIEW_TEXT_COLOR=#363636 PREVIEW_IMG_WIDTH=1200 PREVIEW_IMG_HEIGHT=630 -PREVIEW_DEFAULT_COVER_COLOR="#002549" +PREVIEW_DEFAULT_COVER_COLOR=#002549 diff --git a/.github/workflows/prettier.yaml b/.github/workflows/prettier.yaml new file mode 100644 index 00000000..80696060 --- /dev/null +++ b/.github/workflows/prettier.yaml @@ -0,0 +1,24 @@ +# @url https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions +name: JavaScript Prettier (run ./bw-dev prettier to fix) + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + lint: + name: Lint with Prettier + runs-on: ubuntu-20.04 + + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it. + - uses: actions/checkout@v2 + + - name: Install modules + run: npm install . + + # See .stylelintignore for files that are not linted. + - name: Run Prettier + run: npx prettier --check bookwyrm/static/js/*.js diff --git a/bookwyrm/importers/librarything_import.py b/bookwyrm/importers/librarything_import.py index 1b61a6f1..37730dee 100644 --- a/bookwyrm/importers/librarything_import.py +++ b/bookwyrm/importers/librarything_import.py @@ -14,7 +14,8 @@ class LibrarythingImporter(Importer): """use the dataclass to create the formatted row of data""" remove_brackets = lambda v: re.sub(r"\[|\]", "", v) if v else None normalized = {k: remove_brackets(entry.get(v)) for k, v in mappings.items()} - isbn_13 = normalized["isbn_13"].split(", ") + isbn_13 = normalized.get("isbn_13") + isbn_13 = isbn_13.split(", ") if isbn_13 else [] normalized["isbn_13"] = isbn_13[1] if len(isbn_13) > 0 else None return normalized diff --git a/bookwyrm/migrations/0121_user_summary_keys.py b/bookwyrm/migrations/0121_user_summary_keys.py new file mode 100644 index 00000000..b14c92be --- /dev/null +++ b/bookwyrm/migrations/0121_user_summary_keys.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.5 on 2021-12-22 11:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0120_list_embed_key"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="summary_keys", + field=models.JSONField(null=True), + ), + ] diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index f62678f7..f8d3b781 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -84,6 +84,7 @@ class BookWyrmModel(models.Model): # you can see groups of which you are a member if ( hasattr(self, "memberships") + and viewer.is_authenticated and self.memberships.filter(user=viewer).exists() ): return diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 4d98f5c5..deec2a44 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -148,6 +148,8 @@ class User(OrderedCollectionPageMixin, AbstractUser): size=8, default=get_feed_filter_choices, ) + # annual summary keys + summary_keys = models.JSONField(null=True) preferred_timezone = models.CharField( choices=[(str(tz), str(tz)) for tz in pytz.all_timezones], diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index e95ff96e..0a8e8328 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -14,7 +14,7 @@ VERSION = "0.1.0" PAGE_LENGTH = env("PAGE_LENGTH", 15) DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English") -JS_CACHE = "3891b373" +JS_CACHE = "2d3181e1" # email EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend") @@ -125,9 +125,9 @@ STREAMS = [ DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql_psycopg2", - "NAME": env("POSTGRES_DB", "fedireads"), - "USER": env("POSTGRES_USER", "fedireads"), - "PASSWORD": env("POSTGRES_PASSWORD", "fedireads"), + "NAME": env("POSTGRES_DB", "bookwyrm"), + "USER": env("POSTGRES_USER", "bookwyrm"), + "PASSWORD": env("POSTGRES_PASSWORD", "bookwyrm"), "HOST": env("POSTGRES_HOST", ""), "PORT": env("PGPORT", 5432), }, diff --git a/bookwyrm/static/css/bookwyrm.css b/bookwyrm/static/css/bookwyrm.css index f385e629..5ea65d22 100644 --- a/bookwyrm/static/css/bookwyrm.css +++ b/bookwyrm/static/css/bookwyrm.css @@ -8,6 +8,44 @@ body { flex-direction: column; } +button { + border: none; + margin: 0; + padding: 0; + width: auto; + overflow: visible; + background: transparent; + + /* inherit font, color & alignment from ancestor */ + color: inherit; + font: inherit; + text-align: inherit; + + /* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */ + line-height: normal; + + /* Corrects font smoothing for webkit */ + -webkit-font-smoothing: inherit; + -moz-osx-font-smoothing: inherit; + + /* Corrects inability to style clickable `input` types in iOS */ + -webkit-appearance: none; + + /* Generalizes pointer cursor */ + cursor: pointer; +} + +button::-moz-focus-inner { + /* Remove excess padding and border in Firefox 4+ */ + border: 0; + padding: 0; +} + +/* Better accessibility for keyboard users */ +*:focus-visible { + outline-style: auto !important; +} + .image { overflow: hidden; } @@ -29,10 +67,38 @@ body { overflow-x: auto; } +.modal-card { + pointer-events: none; +} + +.modal-card > * { + pointer-events: all; +} + +/* stylelint-disable no-descending-specificity */ +.modal-card:focus { + outline-style: auto; +} + +.modal-card:focus:not(:focus-visible) { + outline-style: initial; +} + +.modal-card:focus-visible { + outline-style: auto; +} +/* stylelint-enable no-descending-specificity */ + .modal-card.is-fullwidth { min-width: 75% !important; } +@media only screen and (min-width: 769px) { + .modal-card.is-thin { + width: 350px !important; + } +} + .modal-card-body { max-height: 70vh; } @@ -93,6 +159,33 @@ body { display: inline !important; } +button .button-invisible-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + padding: 1rem; + box-sizing: border-box; + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; + background: rgba(0, 0, 0, 0.66); + color: white; + opacity: 0; + transition: opacity 0.2s ease; +} + +button:hover .button-invisible-overlay, +button:active .button-invisible-overlay, +button:focus-visible .button-invisible-overlay { + opacity: 1; +} + +/** File input styles + ******************************************************************************/ + input[type=file]::file-selector-button { -moz-appearance: none; -webkit-appearance: none; @@ -119,17 +212,15 @@ input[type=file]::file-selector-button:hover { color: #363636; } -details .dropdown-menu { - display: block !important; +/** General `details` element styles + ******************************************************************************/ + +summary { + cursor: pointer; } -details.dropdown[open] summary.dropdown-trigger::before { - content: ""; - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; +summary::-webkit-details-marker { + display: none; } summary::marker { @@ -147,6 +238,57 @@ summary::marker { margin-top: 1em; } +/** Details dropdown + ******************************************************************************/ + +details.dropdown[open] summary.dropdown-trigger::before { + content: ""; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; +} + +details .dropdown-menu { + display: block !important; +} + +details .dropdown-menu button { + /* Fix weird Safari defaults */ + box-sizing: border-box; +} + +details.dropdown .dropdown-menu button:focus-visible, +details.dropdown .dropdown-menu a:focus-visible { + outline-style: auto; + outline-offset: -2px; +} + +@media only screen and (max-width: 768px) { + details.dropdown[open] summary.dropdown-trigger::before { + background-color: rgba(0, 0, 0, 0.5); + z-index: 30; + } + + details .dropdown-menu { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex !important; + align-items: center; + justify-content: center; + pointer-events: none; + z-index: 100; + } + + details .dropdown-menu > * { + pointer-events: all; + } +} + /** Shelving ******************************************************************************/ @@ -322,6 +464,8 @@ summary::marker { display: flex; align-items: center; justify-content: center; + flex-direction: column; + gap: 1em; white-space: initial; text-align: center; } @@ -555,6 +699,74 @@ ol.ordered-list li::before { padding: 0 0.75em; } +/* Breadcrumbs + ******************************************************************************/ + +.books-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + align-items: end; + justify-items: stretch; +} + +.books-grid > .is-big { + grid-column: span 2; + grid-row: span 2; + justify-self: stretch; +} + +.books-grid .book-cover { + width: 100%; +} + +.books-grid .book-title { + --height-basis: 1.35rem; + + display: block; + margin-top: 0.5rem; + line-height: var(--height-basis); + min-height: calc(2 * var(--height-basis)); +} + +@media only screen and (min-width: 769px) { + .books-grid { + gap: 1.5rem; + grid-template-columns: repeat(auto-fill, minmax(8em, 1fr)); + } +} + +/* Copy + ******************************************************************************/ + +.horizontal-copy { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.75rem; +} + +.horizontal-copy textarea { + min-width: initial; + white-space: nowrap; +} + +.horizontal-copy button { + align-self: stretch; + height: unset; +} + +.vertical-copy { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 0.75rem; +} + +.vertical-copy button { + width: 100%; +} + /* Dimensions * @todo These could be in rem. ******************************************************************************/ diff --git a/bookwyrm/static/css/fonts/dm_serif_display/OFL.txt b/bookwyrm/static/css/fonts/dm_serif_display/OFL.txt new file mode 100644 index 00000000..929fd66e --- /dev/null +++ b/bookwyrm/static/css/fonts/dm_serif_display/OFL.txt @@ -0,0 +1,95 @@ +Copyright 2014-2018 Adobe (http://www.adobe.com/), with Reserved Font Name +'Source'. All Rights Reserved. Source is a trademark of Adobe in the United +States and/or other countries. Copyright 2019 Google LLC. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/bookwyrm/static/css/fonts/dm_serif_display/dmserifdisplay-italic-webfont.woff b/bookwyrm/static/css/fonts/dm_serif_display/dmserifdisplay-italic-webfont.woff new file mode 100644 index 00000000..a51242ae Binary files /dev/null and b/bookwyrm/static/css/fonts/dm_serif_display/dmserifdisplay-italic-webfont.woff differ diff --git a/bookwyrm/static/css/fonts/dm_serif_display/dmserifdisplay-italic-webfont.woff2 b/bookwyrm/static/css/fonts/dm_serif_display/dmserifdisplay-italic-webfont.woff2 new file mode 100644 index 00000000..a2058d58 Binary files /dev/null and b/bookwyrm/static/css/fonts/dm_serif_display/dmserifdisplay-italic-webfont.woff2 differ diff --git a/bookwyrm/static/css/fonts/dm_serif_display/dmserifdisplay-regular-webfont.woff b/bookwyrm/static/css/fonts/dm_serif_display/dmserifdisplay-regular-webfont.woff new file mode 100644 index 00000000..db28fb1d Binary files /dev/null and b/bookwyrm/static/css/fonts/dm_serif_display/dmserifdisplay-regular-webfont.woff differ diff --git a/bookwyrm/static/css/fonts/dm_serif_display/dmserifdisplay-regular-webfont.woff2 b/bookwyrm/static/css/fonts/dm_serif_display/dmserifdisplay-regular-webfont.woff2 new file mode 100644 index 00000000..ac4cd6b4 Binary files /dev/null and b/bookwyrm/static/css/fonts/dm_serif_display/dmserifdisplay-regular-webfont.woff2 differ diff --git a/bookwyrm/static/css/fonts/dm_serif_display/generator_config.txt b/bookwyrm/static/css/fonts/dm_serif_display/generator_config.txt new file mode 100644 index 00000000..dd0e038c --- /dev/null +++ b/bookwyrm/static/css/fonts/dm_serif_display/generator_config.txt @@ -0,0 +1,30 @@ +# Font Squirrel Font-face Generator Configuration File +# Upload this file to the generator to recreate the settings +# you used to create these fonts. + +{ + "mode": "optimal", + "formats": + [ + "woff", + "woff2" + ], + "tt_instructor": "default", + "fix_gasp": "xy", + "fix_vertical_metrics": "Y", + "metrics_ascent": "", + "metrics_descent": "", + "metrics_linegap": "", + "add_spaces": "Y", + "add_hyphens": "Y", + "fallback": "none", + "fallback_custom": "100", + "options_subset": "basic", + "subset_custom": "", + "subset_custom_range": "", + "subset_ot_features_list": "", + "css_stylesheet": "stylesheet.css", + "filename_suffix": "-webfont", + "emsquare": "2048", + "spacing_adjustment": "0" +} diff --git a/bookwyrm/static/css/vendor/dm_serif_display.css b/bookwyrm/static/css/vendor/dm_serif_display.css new file mode 100644 index 00000000..53a014b0 --- /dev/null +++ b/bookwyrm/static/css/vendor/dm_serif_display.css @@ -0,0 +1,19 @@ +@font-face { + font-family: 'dm_serif_display'; + src: url('../fonts/dm_serif_display/dmserifdisplay-italic-webfont.woff2') format('woff2'), + url('../fonts/dm_serif_display/dmserifdisplay-italic-webfont.woff') format('woff'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'dm_serif_display'; + src: url('../fonts/dm_serif_display/dmserifdisplay-regular-webfont.woff2') format('woff2'), + url('../fonts/dm_serif_display/dmserifdisplay-regular-webfont.woff') format('woff'); + font-weight: normal; + font-style: normal; +} + +.is-serif { + font-family: 'dm_serif_display', Georgia, serif; +} diff --git a/bookwyrm/static/js/block_href.js b/bookwyrm/static/js/block_href.js index fc20a6ab..ac10b6de 100644 --- a/bookwyrm/static/js/block_href.js +++ b/bookwyrm/static/js/block_href.js @@ -1,9 +1,10 @@ /* exported BlockHref */ -let BlockHref = new class { +let BlockHref = new (class { constructor() { - document.querySelectorAll('[data-href]') - .forEach(t => t.addEventListener('click', this.followLink.bind(this))); + document + .querySelectorAll("[data-href]") + .forEach((t) => t.addEventListener("click", this.followLink.bind(this))); } /** @@ -17,5 +18,4 @@ let BlockHref = new class { window.location.href = url; } -}(); - +})(); diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index 2b78bf51..6d21c207 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -1,7 +1,7 @@ /* exported BookWyrm */ /* globals TabGroup */ -let BookWyrm = new class { +let BookWyrm = new (class { constructor() { this.MAX_FILE_SIZE_BYTES = 10 * 1000000; this.initOnDOMLoaded(); @@ -10,48 +10,42 @@ let BookWyrm = new class { } initEventListeners() { - document.querySelectorAll('[data-controls]') - .forEach(button => button.addEventListener( - 'click', - this.toggleAction.bind(this)) - ); + 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(".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(".hidden-form input") + .forEach((button) => button.addEventListener("change", this.revealForm.bind(this))); - document.querySelectorAll('[data-hides]') - .forEach(button => button.addEventListener( - 'change', - this.hideForm.bind(this)) - ); + document + .querySelectorAll("[data-hides]") + .forEach((button) => button.addEventListener("change", this.hideForm.bind(this))); - document.querySelectorAll('[data-back]') - .forEach(button => button.addEventListener( - 'click', - this.back) - ); + document + .querySelectorAll("[data-back]") + .forEach((button) => button.addEventListener("click", this.back)); - document.querySelectorAll('input[type="file"]') - .forEach(node => node.addEventListener( - 'change', - this.disableIfTooLarge.bind(this) - )); - - document.querySelectorAll('[data-duplicate]') - .forEach(node => node.addEventListener( - 'click', - this.duplicateInput.bind(this) - - )) + document + .querySelectorAll('input[type="file"]') + .forEach((node) => node.addEventListener("change", this.disableIfTooLarge.bind(this))); + + document + .querySelectorAll("button[data-modal-open]") + .forEach((node) => node.addEventListener("click", this.handleModalButton.bind(this))); + + document + .querySelectorAll("[data-duplicate]") + .forEach((node) => node.addEventListener("click", this.duplicateInput.bind(this))); + document + .querySelectorAll("details.dropdown") + .forEach((node) => + node.addEventListener("toggle", this.handleDetailsDropdown.bind(this)) + ); } /** @@ -60,15 +54,12 @@ let BookWyrm = new class { initOnDOMLoaded() { const bookwyrm = this; - window.addEventListener('DOMContentLoaded', function() { - document.querySelectorAll('.tab-group') - .forEach(tabs => new TabGroup(tabs)); - document.querySelectorAll('input[type="file"]').forEach( - bookwyrm.disableIfTooLarge.bind(bookwyrm) - ); - document.querySelectorAll('[data-copytext]').forEach( - bookwyrm.copyText.bind(bookwyrm) - ); + window.addEventListener("DOMContentLoaded", function () { + document.querySelectorAll(".tab-group").forEach((tabs) => new TabGroup(tabs)); + document + .querySelectorAll('input[type="file"]') + .forEach(bookwyrm.disableIfTooLarge.bind(bookwyrm)); + document.querySelectorAll("[data-copytext]").forEach(bookwyrm.copyText.bind(bookwyrm)); }); } @@ -77,8 +68,7 @@ let BookWyrm = new class { */ initReccuringTasks() { // Polling - document.querySelectorAll('[data-poll]') - .forEach(liveArea => this.polling(liveArea)); + document.querySelectorAll("[data-poll]").forEach((liveArea) => this.polling(liveArea)); } /** @@ -104,15 +94,19 @@ let BookWyrm = new class { const bookwyrm = this; delay = delay || 10000; - delay += (Math.random() * 1000); + delay += Math.random() * 1000; - setTimeout(function() { - fetch('/api/updates/' + counter.dataset.poll) - .then(response => response.json()) - .then(data => bookwyrm.updateCountElement(counter, data)); + 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); + bookwyrm.polling(counter, delay * 1.25); + }, + delay, + counter + ); } /** @@ -127,60 +121,56 @@ let BookWyrm = new class { const count_by_type = data.count_by_type; const currentCount = counter.innerText; const hasMentions = data.has_mentions; - const allowedStatusTypesEl = document.getElementById('unread-notifications-wrapper'); + const allowedStatusTypesEl = document.getElementById("unread-notifications-wrapper"); // If we're on the right counter element - if (counter.closest('[data-poll-wrapper]').contains(allowedStatusTypesEl)) { + if (counter.closest("[data-poll-wrapper]").contains(allowedStatusTypesEl)) { const allowedStatusTypes = JSON.parse(allowedStatusTypesEl.textContent); // For keys in common between allowedStatusTypes and count_by_type // This concerns 'review', 'quotation', 'comment' - count = allowedStatusTypes.reduce(function(prev, currentKey) { + count = allowedStatusTypes.reduce(function (prev, currentKey) { const currentValue = count_by_type[currentKey] | 0; return prev + currentValue; }, 0); // Add all the "other" in count_by_type if 'everything' is allowed - if (allowedStatusTypes.includes('everything')) { + if (allowedStatusTypes.includes("everything")) { // Clone count_by_type with 0 for reviews/quotations/comments - const count_by_everything_else = Object.assign( - {}, - count_by_type, - {review: 0, quotation: 0, comment: 0} - ); + const count_by_everything_else = Object.assign({}, count_by_type, { + review: 0, + quotation: 0, + comment: 0, + }); - count = Object.keys(count_by_everything_else).reduce( - function(prev, currentKey) { - const currentValue = - count_by_everything_else[currentKey] | 0 + count = Object.keys(count_by_everything_else).reduce(function (prev, currentKey) { + const currentValue = count_by_everything_else[currentKey] | 0; - return prev + currentValue; - }, - count - ); + return prev + currentValue; + }, count); } } if (count != currentCount) { - this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-hidden', count < 1); + this.addRemoveClass(counter.closest("[data-poll-wrapper]"), "is-hidden", count < 1); counter.innerText = count; - this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-danger', hasMentions); + this.addRemoveClass(counter.closest("[data-poll-wrapper]"), "is-danger", hasMentions); } } /** * Show form. - * + * * @param {Event} event * @return {undefined} */ revealForm(event) { let trigger = event.currentTarget; - let hidden = trigger.closest('.hidden-form').querySelectorAll('.is-hidden')[0]; + let hidden = trigger.closest(".hidden-form").querySelectorAll(".is-hidden")[0]; if (hidden) { - this.addRemoveClass(hidden, 'is-hidden', !hidden); + this.addRemoveClass(hidden, "is-hidden", !hidden); } } @@ -192,10 +182,10 @@ let BookWyrm = new class { */ hideForm(event) { let trigger = event.currentTarget; - let targetId = trigger.dataset.hides - let visible = document.getElementById(targetId) + let targetId = trigger.dataset.hides; + let visible = document.getElementById(targetId); - this.addRemoveClass(visible, 'is-hidden', true); + this.addRemoveClass(visible, "is-hidden", true); } /** @@ -210,31 +200,34 @@ let BookWyrm = new class { if (!trigger.dataset.allowDefault || event.currentTarget == event.target) { event.preventDefault(); } - let pressed = trigger.getAttribute('aria-pressed') === 'false'; + 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' - )); + 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')) { + if (targetId && !trigger.classList.contains("pulldown-menu")) { let target = document.getElementById(targetId); - this.addRemoveClass(target, 'is-hidden', !pressed); - this.addRemoveClass(target, 'is-active', pressed); + this.addRemoveClass(target, "is-hidden", !pressed); + this.addRemoveClass(target, "is-active", pressed); } // Show/hide pulldown-menus. - if (trigger.classList.contains('pulldown-menu')) { + if (trigger.classList.contains("pulldown-menu")) { this.toggleMenu(trigger, targetId); } // Show/hide container. - let container = document.getElementById('hide_' + targetId); + let container = document.getElementById("hide_" + targetId); if (container) { this.toggleContainer(container, pressed); @@ -271,14 +264,14 @@ let BookWyrm = new class { * @return {undefined} */ toggleMenu(trigger, targetId) { - let expanded = trigger.getAttribute('aria-expanded') == 'false'; + let expanded = trigger.getAttribute("aria-expanded") == "false"; - trigger.setAttribute('aria-expanded', expanded); + trigger.setAttribute("aria-expanded", expanded); if (targetId) { let target = document.getElementById(targetId); - this.addRemoveClass(target, 'is-active', expanded); + this.addRemoveClass(target, "is-active", expanded); } } @@ -290,7 +283,7 @@ let BookWyrm = new class { * @return {undefined} */ toggleContainer(container, pressed) { - this.addRemoveClass(container, 'is-hidden', pressed); + this.addRemoveClass(container, "is-hidden", pressed); } /** @@ -327,7 +320,7 @@ let BookWyrm = new class { node.focus(); - setTimeout(function() { + setTimeout(function () { node.selectionStart = node.selectionEnd = 10000; }, 0); } @@ -347,15 +340,17 @@ let BookWyrm = new class { 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 - )); + relatedforms.forEach((relatedForm) => + bookwyrm.addRemoveClass( + relatedForm, + "is-hidden", + relatedForm.className.indexOf("is-hidden") == -1 + ) + ); - this.ajaxPost(form).catch(error => { + this.ajaxPost(form).catch((error) => { // @todo Display a notification in the UI instead. - console.warn('Request failed:', error); + console.warn("Request failed:", error); }); } @@ -367,11 +362,11 @@ let BookWyrm = new class { */ ajaxPost(form) { return fetch(form.action, { - method : "POST", + method: "POST", body: new FormData(form), headers: { - 'Accept': 'application/json', - } + Accept: "application/json", + }, }); } @@ -396,24 +391,106 @@ let BookWyrm = new class { const element = eventOrElement.currentTarget || eventOrElement; const submits = element.form.querySelectorAll('[type="submit"]'); - const warns = element.parentElement.querySelectorAll('.file-too-big'); - const isTooBig = element.files && - element.files[0] && - element.files[0].size > MAX_FILE_SIZE_BYTES; + const warns = element.parentElement.querySelectorAll(".file-too-big"); + const isTooBig = + element.files && element.files[0] && element.files[0].size > MAX_FILE_SIZE_BYTES; if (isTooBig) { - submits.forEach(submitter => submitter.disabled = true); - warns.forEach( - sib => addRemoveClass(sib, 'is-hidden', false) - ); + submits.forEach((submitter) => (submitter.disabled = true)); + warns.forEach((sib) => addRemoveClass(sib, "is-hidden", false)); } else { - submits.forEach(submitter => submitter.disabled = false); - warns.forEach( - sib => addRemoveClass(sib, 'is-hidden', true) - ); + submits.forEach((submitter) => (submitter.disabled = false)); + warns.forEach((sib) => addRemoveClass(sib, "is-hidden", true)); } } + /** + * Handle the modal component. + * + * @param {Event} event - Event fired by an element + * with the `data-modal-open` attribute + * pointing to a modal by its id. + * @return {undefined} + * + * See https://github.com/bookwyrm-social/bookwyrm/pull/1633 + * for information about using the modal. + */ + handleModalButton(event) { + const modalButton = event.currentTarget; + const targetModalId = modalButton.dataset.modalOpen; + const htmlElement = document.querySelector("html"); + const modal = document.getElementById(targetModalId); + + if (!modal) { + return; + } + + // Helper functions + function handleModalOpen(modalElement) { + htmlElement.classList.add("is-clipped"); + modalElement.classList.add("is-active"); + modalElement.getElementsByClassName("modal-card")[0].focus(); + + const closeButtons = modalElement.querySelectorAll("[data-modal-close]"); + + closeButtons.forEach((button) => { + button.addEventListener("click", function () { + handleModalClose(modalElement); + }); + }); + + document.addEventListener("keydown", function (event) { + if (event.key === "Escape") { + handleModalClose(modalElement); + } + }); + + modalElement.addEventListener("keydown", handleFocusTrap); + } + + function handleModalClose(modalElement) { + modalElement.removeEventListener("keydown", handleFocusTrap); + htmlElement.classList.remove("is-clipped"); + modalElement.classList.remove("is-active"); + modalButton.focus(); + } + + function handleFocusTrap(event) { + if (event.key !== "Tab") { + return; + } + + const focusableEls = event.currentTarget.querySelectorAll( + [ + "a[href]:not([disabled])", + "button:not([disabled])", + "textarea:not([disabled])", + 'input:not([type="hidden"]):not([disabled])', + "select:not([disabled])", + "details:not([disabled])", + '[tabindex]:not([tabindex="-1"]):not([disabled])', + ].join(",") + ); + const firstFocusableEl = focusableEls[0]; + const lastFocusableEl = focusableEls[focusableEls.length - 1]; + + if (event.shiftKey) { + /* Shift + tab */ if (document.activeElement === firstFocusableEl) { + lastFocusableEl.focus(); + event.preventDefault(); + } + } /* Tab */ else { + if (document.activeElement === lastFocusableEl) { + firstFocusableEl.focus(); + event.preventDefault(); + } + } + } + + // Open modal + handleModalOpen(modal); + } + /** * Display pop up window. * @@ -422,31 +499,27 @@ let BookWyrm = new class { * @return {undefined} */ displayPopUp(url, windowName) { - window.open( - url, - windowName, - "left=100,top=100,width=430,height=600" - ); + window.open(url, windowName, "left=100,top=100,width=430,height=600"); } - duplicateInput (event ) { + duplicateInput(event) { const trigger = event.currentTarget; - const input_id = trigger.dataset['duplicate'] + const input_id = trigger.dataset["duplicate"]; const orig = document.getElementById(input_id); const parent = orig.parentNode; - const new_count = parent.querySelectorAll("input").length + 1 + const new_count = parent.querySelectorAll("input").length + 1; let input = orig.cloneNode(); - input.id += ("-" + (new_count)) - input.value = "" + input.id += "-" + new_count; + input.value = ""; let label = parent.querySelector("label").cloneNode(); - label.setAttribute("for", input.id) + label.setAttribute("for", input.id); - parent.appendChild(label) - parent.appendChild(input) + parent.appendChild(label); + parent.appendChild(input); } /** @@ -461,25 +534,115 @@ let BookWyrm = new class { copyText(textareaEl) { const text = textareaEl.textContent; - const copyButtonEl = document.createElement('button'); + const copyButtonEl = document.createElement("button"); copyButtonEl.textContent = textareaEl.dataset.copytextLabel; - copyButtonEl.classList.add( - "mt-2", - "button", - "is-small", - "is-fullwidth", - "is-primary", - "is-light" - ); - copyButtonEl.addEventListener('click', () => { - navigator.clipboard.writeText(text).then(function() { - textareaEl.classList.add('is-success'); - copyButtonEl.classList.replace('is-primary', 'is-success'); + copyButtonEl.classList.add("button", "is-small", "is-primary", "is-light"); + copyButtonEl.addEventListener("click", () => { + navigator.clipboard.writeText(text).then(function () { + textareaEl.classList.add("is-success"); + copyButtonEl.classList.replace("is-primary", "is-success"); copyButtonEl.textContent = textareaEl.dataset.copytextSuccess; }); }); - textareaEl.parentNode.appendChild(copyButtonEl) + textareaEl.parentNode.appendChild(copyButtonEl); } -}(); + + /** + * Handle the details dropdown component. + * + * @param {Event} event - Event fired by a `details` element + * with the `dropdown` class name, on toggle. + * @return {undefined} + */ + handleDetailsDropdown(event) { + const detailsElement = event.target; + const summaryElement = detailsElement.querySelector("summary"); + const menuElement = detailsElement.querySelector(".dropdown-menu"); + const htmlElement = document.querySelector("html"); + + if (detailsElement.open) { + // Focus first menu element + menuElement + .querySelectorAll("a[href]:not([disabled]), button:not([disabled])")[0] + .focus(); + + // Enable focus trap + menuElement.addEventListener("keydown", this.handleFocusTrap); + + // Close on Esc + detailsElement.addEventListener("keydown", handleEscKey); + + // Clip page if Mobile + if (this.isMobile()) { + htmlElement.classList.add("is-clipped"); + } + } else { + summaryElement.focus(); + + // Disable focus trap + menuElement.removeEventListener("keydown", this.handleFocusTrap); + + // Unclip page + if (this.isMobile()) { + htmlElement.classList.remove("is-clipped"); + } + } + + function handleEscKey(event) { + if (event.key !== "Escape") { + return; + } + + summaryElement.click(); + } + } + + /** + * Check if windows matches mobile media query. + * + * @return {Boolean} + */ + isMobile() { + return window.matchMedia("(max-width: 768px)").matches; + } + + /** + * Focus trap handler + * + * @param {Event} event - Keydown event. + * @return {undefined} + */ + handleFocusTrap(event) { + if (event.key !== "Tab") { + return; + } + + const focusableEls = event.currentTarget.querySelectorAll( + [ + "a[href]:not([disabled])", + "button:not([disabled])", + "textarea:not([disabled])", + 'input:not([type="hidden"]):not([disabled])', + "select:not([disabled])", + "details:not([disabled])", + '[tabindex]:not([tabindex="-1"]):not([disabled])', + ].join(",") + ); + const firstFocusableEl = focusableEls[0]; + const lastFocusableEl = focusableEls[focusableEls.length - 1]; + + if (event.shiftKey) { + /* Shift + tab */ if (document.activeElement === firstFocusableEl) { + lastFocusableEl.focus(); + event.preventDefault(); + } + } /* Tab */ else { + if (document.activeElement === lastFocusableEl) { + firstFocusableEl.focus(); + event.preventDefault(); + } + } + } +})(); diff --git a/bookwyrm/static/js/check_all.js b/bookwyrm/static/js/check_all.js deleted file mode 100644 index fd29f2cd..00000000 --- a/bookwyrm/static/js/check_all.js +++ /dev/null @@ -1,34 +0,0 @@ - -(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('[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 b485ed7e..7d0dc9d8 100644 --- a/bookwyrm/static/js/localstorage.js +++ b/bookwyrm/static/js/localstorage.js @@ -1,13 +1,13 @@ /* exported LocalStorageTools */ /* globals BookWyrm */ -let LocalStorageTools = new class { +let LocalStorageTools = new (class { constructor() { - document.querySelectorAll('[data-hide]') - .forEach(t => this.setDisplay(t)); + document.querySelectorAll("[data-hide]").forEach((t) => this.setDisplay(t)); - document.querySelectorAll('.set-display') - .forEach(t => t.addEventListener('click', this.updateDisplay.bind(this))); + document + .querySelectorAll(".set-display") + .forEach((t) => t.addEventListener("click", this.updateDisplay.bind(this))); } /** @@ -23,8 +23,9 @@ let LocalStorageTools = new class { window.localStorage.setItem(key, value); - document.querySelectorAll('[data-hide="' + key + '"]') - .forEach(node => this.setDisplay(node)); + document + .querySelectorAll('[data-hide="' + key + '"]') + .forEach((node) => this.setDisplay(node)); } /** @@ -38,6 +39,6 @@ let LocalStorageTools = new class { let key = node.dataset.hide; let value = window.localStorage.getItem(key); - BookWyrm.addRemoveClass(node, 'is-hidden', value); + BookWyrm.addRemoveClass(node, "is-hidden", value); } -}(); +})(); diff --git a/bookwyrm/static/js/status_cache.js b/bookwyrm/static/js/status_cache.js index dbc238c4..b19489c1 100644 --- a/bookwyrm/static/js/status_cache.js +++ b/bookwyrm/static/js/status_cache.js @@ -1,22 +1,21 @@ /* exported StatusCache */ /* globals BookWyrm */ -let StatusCache = new class { +let StatusCache = new (class { constructor() { - document.querySelectorAll('[data-cache-draft]') - .forEach(t => t.addEventListener('change', this.updateDraft.bind(this))); + document + .querySelectorAll("[data-cache-draft]") + .forEach((t) => t.addEventListener("change", this.updateDraft.bind(this))); - document.querySelectorAll('[data-cache-draft]') - .forEach(t => this.populateDraft(t)); + document.querySelectorAll("[data-cache-draft]").forEach((t) => this.populateDraft(t)); - document.querySelectorAll('.submit-status') - .forEach(button => button.addEventListener( - 'submit', - this.submitStatus.bind(this)) - ); + document + .querySelectorAll(".submit-status") + .forEach((button) => button.addEventListener("submit", this.submitStatus.bind(this))); - document.querySelectorAll('.form-rate-stars label.icon') - .forEach(button => button.addEventListener('click', this.toggleStar.bind(this))); + document + .querySelectorAll(".form-rate-stars label.icon") + .forEach((button) => button.addEventListener("click", this.toggleStar.bind(this))); } /** @@ -80,25 +79,26 @@ let StatusCache = new class { event.preventDefault(); - BookWyrm.addRemoveClass(form, 'is-processing', true); - trigger.setAttribute('disabled', null); + BookWyrm.addRemoveClass(form, "is-processing", true); + trigger.setAttribute("disabled", null); - BookWyrm.ajaxPost(form).finally(() => { - // Change icon to remove ongoing activity on the current UI. - // Enable back the element used to submit the form. - BookWyrm.addRemoveClass(form, 'is-processing', false); - trigger.removeAttribute('disabled'); - }) - .then(response => { - if (!response.ok) { - throw new Error(); - } - this.submitStatusSuccess(form); - }) - .catch(error => { - console.warn(error); - this.announceMessage('status-error-message'); - }); + BookWyrm.ajaxPost(form) + .finally(() => { + // Change icon to remove ongoing activity on the current UI. + // Enable back the element used to submit the form. + BookWyrm.addRemoveClass(form, "is-processing", false); + trigger.removeAttribute("disabled"); + }) + .then((response) => { + if (!response.ok) { + throw new Error(); + } + this.submitStatusSuccess(form); + }) + .catch((error) => { + console.warn(error); + this.announceMessage("status-error-message"); + }); } /** @@ -112,12 +112,16 @@ let StatusCache = new class { let copy = element.cloneNode(true); copy.id = null; - element.insertAdjacentElement('beforebegin', copy); + element.insertAdjacentElement("beforebegin", copy); - BookWyrm.addRemoveClass(copy, 'is-hidden', false); - setTimeout(function() { - copy.remove(); - }, 10000, copy); + BookWyrm.addRemoveClass(copy, "is-hidden", false); + setTimeout( + function () { + copy.remove(); + }, + 10000, + copy + ); } /** @@ -131,8 +135,9 @@ let StatusCache = new class { form.reset(); // Clear localstorage - form.querySelectorAll('[data-cache-draft]') - .forEach(node => window.localStorage.removeItem(node.dataset.cacheDraft)); + form.querySelectorAll("[data-cache-draft]").forEach((node) => + window.localStorage.removeItem(node.dataset.cacheDraft) + ); // Close modals let modal = form.closest(".modal.is-active"); @@ -142,8 +147,11 @@ let StatusCache = new class { // Update shelve buttons if (form.reading_status) { - document.querySelectorAll("[data-shelve-button-book='" + form.book.value +"']") - .forEach(button => this.cycleShelveButtons(button, form.reading_status.value)); + document + .querySelectorAll("[data-shelve-button-book='" + form.book.value + "']") + .forEach((button) => + this.cycleShelveButtons(button, form.reading_status.value) + ); } return; @@ -156,7 +164,7 @@ let StatusCache = new class { document.querySelector("[data-controls=" + reply.id + "]").click(); } - this.announceMessage('status-success-message'); + this.announceMessage("status-success-message"); } /** @@ -172,8 +180,9 @@ let StatusCache = new class { let next_identifier = shelf.dataset.shelfNext; // Set all buttons to hidden - button.querySelectorAll("[data-shelf-identifier]") - .forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", true)); + button + .querySelectorAll("[data-shelf-identifier]") + .forEach((item) => BookWyrm.addRemoveClass(item, "is-hidden", true)); // Button that should be visible now let next = button.querySelector("[data-shelf-identifier=" + next_identifier + "]"); @@ -183,15 +192,17 @@ let StatusCache = new class { // ------ update the dropdown buttons // Remove existing hidden class - button.querySelectorAll("[data-shelf-dropdown-identifier]") - .forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", false)); + button + .querySelectorAll("[data-shelf-dropdown-identifier]") + .forEach((item) => BookWyrm.addRemoveClass(item, "is-hidden", false)); // Remove existing disabled states - button.querySelectorAll("[data-shelf-dropdown-identifier] button") - .forEach(item => item.disabled = false); + button + .querySelectorAll("[data-shelf-dropdown-identifier] button") + .forEach((item) => (item.disabled = false)); - next_identifier = next_identifier == 'complete' ? 'read' : next_identifier; + next_identifier = next_identifier == "complete" ? "read" : next_identifier; // Disable the current state button.querySelector( @@ -206,8 +217,9 @@ let StatusCache = new class { BookWyrm.addRemoveClass(main_button, "is-hidden", true); // Just hide the other two menu options, idk what to do with them - button.querySelectorAll("[data-extra-options]") - .forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", true)); + button + .querySelectorAll("[data-extra-options]") + .forEach((item) => BookWyrm.addRemoveClass(item, "is-hidden", true)); // Close menu let menu = button.querySelector("details[open]"); @@ -235,5 +247,4 @@ let StatusCache = new class { halfStar.checked = "checked"; } } -}(); - +})(); diff --git a/bookwyrm/templates/annual_summary/layout.html b/bookwyrm/templates/annual_summary/layout.html new file mode 100644 index 00000000..b5ba9cc7 --- /dev/null +++ b/bookwyrm/templates/annual_summary/layout.html @@ -0,0 +1,273 @@ +{% extends 'layout.html' %} +{% load i18n %} +{% load static %} +{% load humanize %} + + +{% block title %}{% blocktrans %}{{ year }} in the books{% endblocktrans %}{% endblock %} + +{% block head_links %} + +{% endblock %} + +{% block content %} +{% with display_name=summary_user.display_name %} +{% if user == summary_user %} +
+ {% with year=paginated_years|first %} + {% if year %} +
+ + + {{ year }} + +
+ {% endif %} + {% endwith %} + + {% with year=paginated_years|last %} + {% if year %} +
+ + {{ year }} + + +
+ {% endif %} + {% endwith %} +
+{% endif %} + +

+ 📚✨ + {% blocktrans %}{{ year }} in the books{% endblocktrans %} + ✨📚 +

+

+ {% blocktrans %}{{ display_name }}’s year of reading{% endblocktrans %} +

+ +
+ + + {% trans "Share this page" %} + + +
+
+ + {% if year_key %} +
+ +
+ {% endif %} + + {% if user == summary_user %} + {% if year_key %} +
+
+

{% trans "Sharing status: public with key" %}

+

{% trans "The page can be seen by anyone with the complete address." %}

+
+
+ {% csrf_token %} + + +
+
+ {% else %} +
+
+

{% trans "Sharing status: private" %}

+

{% trans "The page is private, only you can see it." %}

+
+
+ {% csrf_token %} + + +
+
+ {% endif %} +

{% trans "When you make your page private, the old key won’t give access to the page anymore. A new key will be created if the page is once again made public." %}

+ {% endif %} +
+
+
+ +
+
+
+
+
+ +{% if not books %} +

{% blocktrans %}Sadly {{ display_name }} didn’t finish any book in {{ year }}{% endblocktrans %}

+{% else %} + +
+
+

+ {% blocktrans trimmed count counter=books_total with pages_total=pages_total|intcomma %} + In {{ year }}, {{ display_name }} read {{ books_total }} book
for a total of {{ pages_total }} pages! + {% plural %} + In {{ year }}, {{ display_name }} read {{ books_total }} books
for a total of {{ pages_total }} pages! + {% endblocktrans %} +

+

{% trans "That’s great!" %}

+ +

+ {% blocktrans with pages=pages_average|intcomma %}That makes an average of {{ pages }} pages per book.{% endblocktrans %} +

+ + {% if no_page_number %} +

+ {% blocktrans trimmed count counter=no_page_number %} + ({{ no_page_number }} book doesn’t have pages) + {% plural %} + ({{ no_page_number }} books don’t have pages) + {% endblocktrans %} +

+ {% endif %} +
+
+ + {% if book_pages_lowest and book_pages_highest %} +
+
+ {% include 'snippets/book_cover.html' with book=book_pages_lowest cover_class='is-w-auto-tablet is-h-l-mobile' size='xxlarge' %} +
+
+ {% trans "Their shortest read this year…" %} +

+ + {{ book_pages_lowest.title }} + +

+ {% if book_pages_lowest.authors.exists %} +

{% trans "by" %} + {% include 'snippets/authors.html' with book=book_pages_lowest link_class="has-text-success-dark" %} +

+ {% endif %} +

+ {% with pages=book_pages_lowest.pages %} + {% blocktrans %}{{ pages }} pages{% endblocktrans%} + {% endwith %} +

+
+
+ {% include 'snippets/book_cover.html' with book=book_pages_highest cover_class='is-w-auto-tablet is-h-l-mobile' size='xxlarge' %} +
+
+ {% trans "…and the longest" %} +

+ + {{ book_pages_highest.title }} + +

+ {% if book_pages_highest.authors.exists %} +

{% trans "by" %} + {% include 'snippets/authors.html' with book=book_pages_highest link_class="has-text-success-dark" %} +

+ {% endif %} +

+ {% with pages=book_pages_highest.pages %} + {% blocktrans %}{{ pages }} pages{% endblocktrans%} + {% endwith %} +

+
+
+ {% endif %} + +
+
+
+
+
+ + {% if ratings_total > 0 %} +
+
+

+ {% blocktrans trimmed count counter=ratings_total %} + {{ display_name }} left {{ ratings_total }} rating,
their average rating is {{ rating_average }} + {% plural %} + {{ display_name }} left {{ ratings_total }} ratings,
their average rating is {{ rating_average }} + {% endblocktrans %} +

+
+
+
+
+ {% include 'snippets/book_cover.html' with book=book_rating_highest.book cover_class='is-w-xl-tablet is-h-l-mobile' size='xxlarge' %} +
+ {% if book_rating_highest %} +
+ {% trans "Their best rated review" %} +

+ + {{ book_rating_highest.book.title }} + +

+ {% if book_rating_highest.book.authors.exists %} +

{% trans "by" %} + {% include 'snippets/authors.html' with book=book_rating_highest.book link_class="has-text-success-dark" %} +

+ {% endif %} +

+ {% with rating=book_rating_highest.rating|floatformat %} + {% blocktrans %}Their rating: {{ rating }}{% endblocktrans%} + {% endwith %} +

+
+ {% endif %} +
+ +
+
+
+
+
+ {% endif %} + +
+
+

+ {% blocktrans %}All the books {{ display_name }} read in {{ year }}{% endblocktrans %} +

+
+
+ +
+
+ +
+
+{% endif %} +{% endwith %} +{% endblock %} diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index 27d061a2..4903da87 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -61,24 +61,48 @@
- {% include 'snippets/book_cover.html' with size='xxlarge' size_mobile='medium' book=book cover_class='is-h-m-mobile' %} + {% if not book.cover %} + {% if user_authenticated %} + + {% include 'book/cover_add_modal.html' with book=book controls_text="add_cover" controls_uid=book.id %} + {% if request.GET.cover_error %} +

{% trans "Failed to load cover" %}

+ {% endif %} + {% else %} + {% include 'snippets/book_cover.html' with book=book cover_class='is-h-m-mobile' %} + {% endif %} + {% endif %} + + {% if book.cover %} + + {% include 'book/cover_show_modal.html' with book=book id="cover_show_modal" %} + {% endif %} + {% include 'snippets/rate_action.html' with user=request.user book=book %}
{% include 'snippets/shelve_button/shelve_button.html' %}
- {% if user_authenticated and not book.cover %} -
- {% trans "Add cover" as button_text %} - {% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="add_cover" controls_uid=book.id focus="modal_title_add_cover" class="is-small" %} - {% include 'book/cover_modal.html' with book=book controls_text="add_cover" controls_uid=book.id %} - {% if request.GET.cover_error %} -

{% trans "Failed to load cover" %}

- {% endif %} -
- {% endif %} -
{% with book=book %}
diff --git a/bookwyrm/templates/book/cover_modal.html b/bookwyrm/templates/book/cover_add_modal.html similarity index 100% rename from bookwyrm/templates/book/cover_modal.html rename to bookwyrm/templates/book/cover_add_modal.html diff --git a/bookwyrm/templates/book/cover_show_modal.html b/bookwyrm/templates/book/cover_show_modal.html new file mode 100644 index 00000000..f244aa53 --- /dev/null +++ b/bookwyrm/templates/book/cover_show_modal.html @@ -0,0 +1,12 @@ +{% load i18n %} +{% load static %} + + diff --git a/bookwyrm/templates/components/dropdown.html b/bookwyrm/templates/components/dropdown.html index b3710271..afa6d050 100644 --- a/bookwyrm/templates/components/dropdown.html +++ b/bookwyrm/templates/components/dropdown.html @@ -7,6 +7,7 @@ class=" dropdown control {% if right %}is-right{% endif %} + has-text-left " > -
diff --git a/bookwyrm/templates/search/layout.html b/bookwyrm/templates/search/layout.html index 239586ef..af912945 100644 --- a/bookwyrm/templates/search/layout.html +++ b/bookwyrm/templates/search/layout.html @@ -13,7 +13,7 @@
- +
diff --git a/bookwyrm/templates/settings/announcements/announcement.html b/bookwyrm/templates/settings/announcements/announcement.html index 37dba06f..8b49f4f4 100644 --- a/bookwyrm/templates/settings/announcements/announcement.html +++ b/bookwyrm/templates/settings/announcements/announcement.html @@ -31,35 +31,29 @@
-
-
{% trans "Visible:" %}
-
- {% if announcement in active_announcements %} - {% trans "True" %} - {% else %} - {% trans "False" %} - {% endif %} -
-
+
{% trans "Visible:" %}
+
+ + {% if announcement in active_announcements %} + {% trans "True" %} + {% else %} + {% trans "False" %} + {% endif %} + +
{% if announcement.start_date %} -
-
{% trans "Start date:" %}
-
{{ announcement.start_date|naturalday }}
-
+
{% trans "Start date:" %}
+
{{ announcement.start_date|naturalday }}
{% endif %} {% if announcement.end_date %} -
-
{% trans "End date:" %}
-
{{ announcement.end_date|naturalday }}
-
+
{% trans "End date:" %}
+
{{ announcement.end_date|naturalday }}
{% endif %} -
-
{% trans "Active:" %}
-
{{ announcement.active }}
-
+
{% trans "Active:" %}
+
{{ announcement.active }}
diff --git a/bookwyrm/templates/snippets/authors.html b/bookwyrm/templates/snippets/authors.html index 81aaa138..69136adf 100644 --- a/bookwyrm/templates/snippets/authors.html +++ b/bookwyrm/templates/snippets/authors.html @@ -13,7 +13,7 @@ {% for author in book.authors.all|slice:limit %}