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
- '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 {