diff --git a/styleguide/src/system/styles/fonts/gentium-basic.scss b/styleguide/src/system/styles/fonts/gentium-basic.scss index 04628b6e0..92c52a5e3 100644 --- a/styleguide/src/system/styles/fonts/gentium-basic.scss +++ b/styleguide/src/system/styles/fonts/gentium-basic.scss @@ -7,6 +7,7 @@ url('~@@/assets/fonts/gentium-basic/GentiumBasic.woff2') format('woff2'), url('~@@/assets/fonts/gentium-basic/GentiumBasic.woff') format('woff'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + font-display: swap; } @font-face { @@ -18,4 +19,5 @@ url('~@@/assets/fonts/gentium-basic/GentiumBasic-Italic.woff2') format('woff2'), url('~@@/assets/fonts/gentium-basic/GentiumBasic-Italic.woff') format('woff'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + font-display: swap; } diff --git a/styleguide/src/system/styles/fonts/lato.scss b/styleguide/src/system/styles/fonts/lato.scss index aceb39e49..efa520571 100644 --- a/styleguide/src/system/styles/fonts/lato.scss +++ b/styleguide/src/system/styles/fonts/lato.scss @@ -6,6 +6,7 @@ font-style: normal; font-weight: 600; text-rendering: optimizeLegibility; + font-display: swap; } /* Webfont: Lato-BoldItalic */ @@ -16,6 +17,7 @@ font-style: italic; font-weight: 600; text-rendering: optimizeLegibility; + font-display: swap; } /* Webfont: Lato-Italic */ @@ -26,6 +28,7 @@ font-style: italic; font-weight: normal; text-rendering: optimizeLegibility; + font-display: swap; } /* Webfont: Lato-Regular */ @@ -36,4 +39,5 @@ font-style: normal; font-weight: normal; text-rendering: optimizeLegibility; + font-display: swap; } diff --git a/webapp/components/DonationInfo/DonationInfo.vue b/webapp/components/DonationInfo/DonationInfo.vue index b99a30993..a80487c68 100644 --- a/webapp/components/DonationInfo/DonationInfo.vue +++ b/webapp/components/DonationInfo/DonationInfo.vue @@ -56,5 +56,6 @@ export default { margin-bottom: $space-x-small; margin-top: 16px; cursor: pointer; + color: $text-color-base; } diff --git a/webapp/components/Dropdown.vue b/webapp/components/Dropdown.vue index 3faa1824d..389e7ced4 100644 --- a/webapp/components/Dropdown.vue +++ b/webapp/components/Dropdown.vue @@ -45,6 +45,7 @@ export default { watch: { isPopoverOpen: { handler(isOpen) { + if (typeof document === 'undefined') return if (isOpen) { document.body.classList.add('dropdown-open') } else { @@ -58,7 +59,9 @@ export default { clearTimeout(mouseLeaveTimer) if (this.isPopoverOpen) { this.isPopoverOpen = false - document.body.classList.remove('dropdown-open') + if (typeof document !== 'undefined') { + document.body.classList.remove('dropdown-open') + } } }, methods: { diff --git a/webapp/components/MasonryGrid/MasonryGrid.spec.js b/webapp/components/MasonryGrid/MasonryGrid.spec.js index afc0892d9..22c997d40 100644 --- a/webapp/components/MasonryGrid/MasonryGrid.spec.js +++ b/webapp/components/MasonryGrid/MasonryGrid.spec.js @@ -4,6 +4,13 @@ import MasonryGrid from './MasonryGrid' const localVue = global.localVue +const GridChild = { + template: '
child
', + data() { + return { rowSpan: 0 } + }, +} + describe('MasonryGrid', () => { let wrapper @@ -11,29 +18,66 @@ describe('MasonryGrid', () => { wrapper = mount(MasonryGrid, { localVue }) }) - it('adds the "reset-grid-height" class when itemsCalculating is more than 0', async () => { - wrapper.setData({ itemsCalculating: 1 }) + it('adds the "reset-grid-height" class when measuring is true', async () => { + wrapper.setData({ measuring: true }) await Vue.nextTick() expect(wrapper.classes()).toContain('reset-grid-height') }) - it('removes the "reset-grid-height" class when itemsCalculating is 0', async () => { - wrapper.setData({ itemsCalculating: 0 }) + it('removes the "reset-grid-height" class when measuring is false', async () => { + wrapper.setData({ measuring: false }) await Vue.nextTick() expect(wrapper.classes()).not.toContain('reset-grid-height') }) - it('adds 1 to itemsCalculating when "calculating-item-height" is emitted', async () => { - wrapper.setData({ itemsCalculating: 0 }) - wrapper.vm.$emit('calculating-item-height') - await Vue.nextTick() - expect(wrapper.vm.itemsCalculating).toBe(1) + it('sets inline grid styles', () => { + expect(wrapper.element.style.gridAutoRows).toBe('2px') + expect(wrapper.element.style.rowGap).toBe('2px') }) - it('subtracts 1 from itemsCalculating when "finished-calculating-item-height" is emitted', async () => { - wrapper.setData({ itemsCalculating: 2 }) - wrapper.vm.$emit('finished-calculating-item-height') + it('calculates rowSpan for children via batchRecalculate', async () => { + wrapper = mount(MasonryGrid, { + localVue, + slots: { default: GridChild }, + }) + + const child = wrapper.vm.$children[0] + Object.defineProperty(child.$el, 'clientHeight', { value: 100, configurable: true }) + + await wrapper.vm.batchRecalculate() + + // Math.ceil((100 + 2) / (2 + 2)) = Math.ceil(25.5) = 26 + expect(child.rowSpan).toBe(26) + expect(wrapper.vm.measuring).toBe(false) + }) + + it('recalculates when child count changes in updated()', async () => { + const Parent = { + template: '', + components: { MasonryGrid, GridChild }, + data() { + return { count: 1 } + }, + } + wrapper = mount(Parent, { localVue }) await Vue.nextTick() - expect(wrapper.vm.itemsCalculating).toBe(1) + expect(wrapper.vm.$children[0].childCount).toBe(1) + + wrapper.setData({ count: 2 }) + await Vue.nextTick() + await Vue.nextTick() + expect(wrapper.vm.$children[0].childCount).toBe(2) + }) + + it('skips children without rowSpan', async () => { + const NoRowSpan = { template: '
no rowSpan
' } + wrapper = mount(MasonryGrid, { + localVue, + slots: { default: NoRowSpan }, + }) + + await wrapper.vm.batchRecalculate() + + expect(wrapper.vm.measuring).toBe(false) }) }) diff --git a/webapp/components/MasonryGrid/MasonryGrid.vue b/webapp/components/MasonryGrid/MasonryGrid.vue index e750e4ccc..a85291aad 100644 --- a/webapp/components/MasonryGrid/MasonryGrid.vue +++ b/webapp/components/MasonryGrid/MasonryGrid.vue @@ -1,61 +1,73 @@ diff --git a/webapp/components/MasonryGrid/MasonryGridItem.vue b/webapp/components/MasonryGrid/MasonryGridItem.vue index f01f39404..f85668923 100644 --- a/webapp/components/MasonryGrid/MasonryGridItem.vue +++ b/webapp/components/MasonryGrid/MasonryGridItem.vue @@ -27,24 +27,5 @@ export default { rowSpan: this.imageAspectRatio ? getRowSpan(this.imageAspectRatio) : 69, } }, - methods: { - calculateItemHeight() { - this.$parent.$emit('calculating-item-height') - - this.$nextTick(() => { - const gridStyle = this.$parent.$el.style - const rowHeight = parseInt(gridStyle.gridAutoRows) - const rowGapValue = gridStyle.rowGap || gridStyle.gridRowGap - const rowGap = parseInt(rowGapValue) - const itemHeight = this.$el.clientHeight - - this.rowSpan = Math.ceil((itemHeight + rowGap) / (rowHeight + rowGap)) - this.$parent.$emit('finished-calculating-item-height') - }) - }, - }, - mounted() { - this.calculateItemHeight() - }, } diff --git a/webapp/components/PostTeaser/PostTeaser.vue b/webapp/components/PostTeaser/PostTeaser.vue index e6c28d9bd..78bbec138 100644 --- a/webapp/components/PostTeaser/PostTeaser.vue +++ b/webapp/components/PostTeaser/PostTeaser.vue @@ -12,8 +12,17 @@ :highlight="isPinned" > @@ -207,6 +216,11 @@ export default { default: false, }, }, + data() { + return { + imageLoaded: false, + } + }, computed: { ...mapGetters({ user: 'auth/user', @@ -303,6 +317,18 @@ export default { } } +@keyframes image-placeholder-pulse { + 0% { + opacity: 0.6; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.6; + } +} + .post-user-row { position: relative; @@ -335,7 +361,12 @@ export default { .image-placeholder { width: 100%; - background-color: $color-neutral-80; + background-color: $background-color-softer; + animation: image-placeholder-pulse 1.5s ease-in-out infinite; + + &--loaded { + animation: none; + } > .image { display: block; diff --git a/webapp/components/ResponsiveImage/ResponsiveImage.vue b/webapp/components/ResponsiveImage/ResponsiveImage.vue index 8fa3bb89b..b86042d45 100644 --- a/webapp/components/ResponsiveImage/ResponsiveImage.vue +++ b/webapp/components/ResponsiveImage/ResponsiveImage.vue @@ -1,9 +1,20 @@ + + diff --git a/webapp/components/UserTeaser/__snapshots__/UserTeaser.spec.js.snap b/webapp/components/UserTeaser/__snapshots__/UserTeaser.spec.js.snap index 55460f758..ffa7273d1 100644 --- a/webapp/components/UserTeaser/__snapshots__/UserTeaser.spec.js.snap +++ b/webapp/components/UserTeaser/__snapshots__/UserTeaser.spec.js.snap @@ -190,7 +190,9 @@ exports[`UserTeaser given an user user is disabled current user is a moderator r Tilda Swinton - 'ontouchstart' in window || navigator.MaxTouchPoints > 0 || navigator.msMaxTouchPoints > 0 + typeof window !== 'undefined' && + ('ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0) diff --git a/webapp/graphql/PostQuery.js b/webapp/graphql/PostQuery.js index c0754f0c1..2f7cb042e 100644 --- a/webapp/graphql/PostQuery.js +++ b/webapp/graphql/PostQuery.js @@ -52,7 +52,30 @@ export default (i18n) => { ` } -export const filterPosts = (i18n) => { +export const filterPosts = () => { + return gql` + ${user} + ${post} + ${postCounts} + ${tagsCategoriesAndPinned} + + query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) { + Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) { + postType + eventStart + eventEnd + eventVenue + eventLocationName + eventIsOnline + ...post + ...postCounts + ...tagsCategoriesAndPinned + } + } + ` +} + +export const filterMapPosts = (i18n) => { const lang = i18n.locale().toUpperCase() return gql` ${user} diff --git a/webapp/nuxt.config.js b/webapp/nuxt.config.js index 02edbf9dc..fbd1bf4d8 100644 --- a/webapp/nuxt.config.js +++ b/webapp/nuxt.config.js @@ -239,6 +239,12 @@ export default { }, manifest, + + render: { + // Generate preload hints for critical JS/CSS/font assets + resourceHints: true, + }, + /* ** Build configuration */ diff --git a/webapp/pages/index.vue b/webapp/pages/index.vue index 8b01b82cf..058e5d2aa 100644 --- a/webapp/pages/index.vue +++ b/webapp/pages/index.vue @@ -344,7 +344,7 @@ export default { }, Post: { query() { - return filterPosts(this.$i18n) + return filterPosts() }, variables() { return { diff --git a/webapp/pages/map.vue b/webapp/pages/map.vue index db6b82cb8..b74ec9f30 100644 --- a/webapp/pages/map.vue +++ b/webapp/pages/map.vue @@ -66,7 +66,7 @@ import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css' import { mapGetters } from 'vuex' import { profileUserQuery, mapUserQuery } from '~/graphql/User' import { groupQuery } from '~/graphql/groups' -import { filterPosts } from '~/graphql/PostQuery.js' +import { filterMapPosts } from '~/graphql/PostQuery.js' import mobile from '~/mixins/mobile' import Empty from '~/components/Empty/Empty' import MapStylesButtons from '~/components/Map/MapStylesButtons' @@ -542,7 +542,7 @@ export default { }, Post: { query() { - return filterPosts(this.$i18n) + return filterMapPosts(this.$i18n) }, variables() { return {