mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2026-03-01 12:44:37 +00:00
fix(webapp): optimize masonry grid rendering and add SSR compatibility (#9284)
This commit is contained in:
parent
e3a41cb828
commit
6b6e77c2a5
@ -7,6 +7,7 @@
|
|||||||
url('~@@/assets/fonts/gentium-basic/GentiumBasic.woff2') format('woff2'),
|
url('~@@/assets/fonts/gentium-basic/GentiumBasic.woff2') format('woff2'),
|
||||||
url('~@@/assets/fonts/gentium-basic/GentiumBasic.woff') format('woff');
|
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;
|
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 {
|
@font-face {
|
||||||
@ -18,4 +19,5 @@
|
|||||||
url('~@@/assets/fonts/gentium-basic/GentiumBasic-Italic.woff2') format('woff2'),
|
url('~@@/assets/fonts/gentium-basic/GentiumBasic-Italic.woff2') format('woff2'),
|
||||||
url('~@@/assets/fonts/gentium-basic/GentiumBasic-Italic.woff') format('woff');
|
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;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Webfont: Lato-BoldItalic */
|
/* Webfont: Lato-BoldItalic */
|
||||||
@ -16,6 +17,7 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Webfont: Lato-Italic */
|
/* Webfont: Lato-Italic */
|
||||||
@ -26,6 +28,7 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Webfont: Lato-Regular */
|
/* Webfont: Lato-Regular */
|
||||||
@ -36,4 +39,5 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,5 +56,6 @@ export default {
|
|||||||
margin-bottom: $space-x-small;
|
margin-bottom: $space-x-small;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
color: $text-color-base;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
isPopoverOpen: {
|
isPopoverOpen: {
|
||||||
handler(isOpen) {
|
handler(isOpen) {
|
||||||
|
if (typeof document === 'undefined') return
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
document.body.classList.add('dropdown-open')
|
document.body.classList.add('dropdown-open')
|
||||||
} else {
|
} else {
|
||||||
@ -58,7 +59,9 @@ export default {
|
|||||||
clearTimeout(mouseLeaveTimer)
|
clearTimeout(mouseLeaveTimer)
|
||||||
if (this.isPopoverOpen) {
|
if (this.isPopoverOpen) {
|
||||||
this.isPopoverOpen = false
|
this.isPopoverOpen = false
|
||||||
document.body.classList.remove('dropdown-open')
|
if (typeof document !== 'undefined') {
|
||||||
|
document.body.classList.remove('dropdown-open')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@ -4,6 +4,13 @@ import MasonryGrid from './MasonryGrid'
|
|||||||
|
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
const GridChild = {
|
||||||
|
template: '<div>child</div>',
|
||||||
|
data() {
|
||||||
|
return { rowSpan: 0 }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
describe('MasonryGrid', () => {
|
describe('MasonryGrid', () => {
|
||||||
let wrapper
|
let wrapper
|
||||||
|
|
||||||
@ -11,29 +18,66 @@ describe('MasonryGrid', () => {
|
|||||||
wrapper = mount(MasonryGrid, { localVue })
|
wrapper = mount(MasonryGrid, { localVue })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('adds the "reset-grid-height" class when itemsCalculating is more than 0', async () => {
|
it('adds the "reset-grid-height" class when measuring is true', async () => {
|
||||||
wrapper.setData({ itemsCalculating: 1 })
|
wrapper.setData({ measuring: true })
|
||||||
await Vue.nextTick()
|
await Vue.nextTick()
|
||||||
expect(wrapper.classes()).toContain('reset-grid-height')
|
expect(wrapper.classes()).toContain('reset-grid-height')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('removes the "reset-grid-height" class when itemsCalculating is 0', async () => {
|
it('removes the "reset-grid-height" class when measuring is false', async () => {
|
||||||
wrapper.setData({ itemsCalculating: 0 })
|
wrapper.setData({ measuring: false })
|
||||||
await Vue.nextTick()
|
await Vue.nextTick()
|
||||||
expect(wrapper.classes()).not.toContain('reset-grid-height')
|
expect(wrapper.classes()).not.toContain('reset-grid-height')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('adds 1 to itemsCalculating when "calculating-item-height" is emitted', async () => {
|
it('sets inline grid styles', () => {
|
||||||
wrapper.setData({ itemsCalculating: 0 })
|
expect(wrapper.element.style.gridAutoRows).toBe('2px')
|
||||||
wrapper.vm.$emit('calculating-item-height')
|
expect(wrapper.element.style.rowGap).toBe('2px')
|
||||||
await Vue.nextTick()
|
|
||||||
expect(wrapper.vm.itemsCalculating).toBe(1)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('subtracts 1 from itemsCalculating when "finished-calculating-item-height" is emitted', async () => {
|
it('calculates rowSpan for children via batchRecalculate', async () => {
|
||||||
wrapper.setData({ itemsCalculating: 2 })
|
wrapper = mount(MasonryGrid, {
|
||||||
wrapper.vm.$emit('finished-calculating-item-height')
|
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: '<MasonryGrid><GridChild v-for="n in count" :key="n" /></MasonryGrid>',
|
||||||
|
components: { MasonryGrid, GridChild },
|
||||||
|
data() {
|
||||||
|
return { count: 1 }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
wrapper = mount(Parent, { localVue })
|
||||||
await Vue.nextTick()
|
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: '<div>no rowSpan</div>' }
|
||||||
|
wrapper = mount(MasonryGrid, {
|
||||||
|
localVue,
|
||||||
|
slots: { default: NoRowSpan },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.vm.batchRecalculate()
|
||||||
|
|
||||||
|
expect(wrapper.vm.measuring).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,61 +1,73 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="ds-grid" :style="gridStyle" :class="[itemsCalculating ? 'reset-grid-height' : '']">
|
<div
|
||||||
|
class="ds-grid"
|
||||||
|
:style="{ gridAutoRows: '2px', rowGap: '2px' }"
|
||||||
|
:class="[measuring ? 'reset-grid-height' : '']"
|
||||||
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const ROW_HEIGHT = 2
|
||||||
|
const ROW_GAP = 2
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
itemsCalculating: 0,
|
measuring: false,
|
||||||
isMobile: false,
|
childCount: 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
gridStyle() {
|
|
||||||
const size = this.isMobile ? '1px' : '2px'
|
|
||||||
return { gridAutoRows: size, rowGap: size }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
isMobile() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$children.forEach((child) => {
|
|
||||||
if (child.calculateItemHeight) child.calculateItemHeight()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.$on('calculating-item-height', this.startCalculation)
|
|
||||||
this.$on('finished-calculating-item-height', this.endCalculation)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
startCalculation() {
|
|
||||||
this.itemsCalculating += 1
|
|
||||||
},
|
|
||||||
endCalculation() {
|
|
||||||
this.itemsCalculating -= 1
|
|
||||||
},
|
|
||||||
checkMobile() {
|
|
||||||
this.isMobile = window.innerWidth <= 810
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.checkMobile()
|
this.$nextTick(() => this.batchRecalculate())
|
||||||
// Children mount before parent — recalculate their spans with correct grid values
|
this._resizeTimer = null
|
||||||
this.$nextTick(() => {
|
this._onResize = () => {
|
||||||
this.$children.forEach((child) => {
|
clearTimeout(this._resizeTimer)
|
||||||
if (child.calculateItemHeight) child.calculateItemHeight()
|
this._resizeTimer = setTimeout(() => this.batchRecalculate(), 150)
|
||||||
})
|
}
|
||||||
})
|
window.addEventListener('resize', this._onResize)
|
||||||
window.addEventListener('resize', this.checkMobile)
|
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$off('calculating-item-height', this.startCalculation)
|
clearTimeout(this._resizeTimer)
|
||||||
this.$off('finished-calculating-item-height', this.endCalculation)
|
window.removeEventListener('resize', this._onResize)
|
||||||
window.removeEventListener('resize', this.checkMobile)
|
},
|
||||||
|
updated() {
|
||||||
|
const count = this.$children.length
|
||||||
|
if (count !== this.childCount) {
|
||||||
|
this.batchRecalculate()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async batchRecalculate() {
|
||||||
|
this._recalcId = (this._recalcId || 0) + 1
|
||||||
|
const id = this._recalcId
|
||||||
|
|
||||||
|
this.childCount = this.$children.length
|
||||||
|
// Switch to auto-height so items take their natural height
|
||||||
|
this.measuring = true
|
||||||
|
|
||||||
|
await this.$nextTick()
|
||||||
|
|
||||||
|
// A newer call has started — let it handle the measurement
|
||||||
|
if (id !== this._recalcId) return
|
||||||
|
|
||||||
|
// Read pass: measure all children in one go (single reflow)
|
||||||
|
const measurements = this.$children.map((child) => ({
|
||||||
|
child,
|
||||||
|
height: child.$el.clientHeight,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Write pass: set all rowSpans (no interleaved reads)
|
||||||
|
measurements.forEach(({ child, height }) => {
|
||||||
|
if (child.rowSpan !== undefined) {
|
||||||
|
child.rowSpan = Math.ceil((height + ROW_GAP) / (ROW_HEIGHT + ROW_GAP))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Switch back to fixed row grid
|
||||||
|
this.measuring = false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -27,24 +27,5 @@ export default {
|
|||||||
rowSpan: this.imageAspectRatio ? getRowSpan(this.imageAspectRatio) : 69,
|
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()
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -12,8 +12,17 @@
|
|||||||
:highlight="isPinned"
|
:highlight="isPinned"
|
||||||
>
|
>
|
||||||
<template v-if="post.image" #heroImage>
|
<template v-if="post.image" #heroImage>
|
||||||
<div class="image-placeholder" :style="{ aspectRatio: post.image.aspectRatio }">
|
<div
|
||||||
<responsive-image :image="post.image" sizes="640px" class="image" />
|
class="image-placeholder"
|
||||||
|
:class="{ 'image-placeholder--loaded': imageLoaded }"
|
||||||
|
:style="{ aspectRatio: post.image.aspectRatio }"
|
||||||
|
>
|
||||||
|
<responsive-image
|
||||||
|
:image="post.image"
|
||||||
|
sizes="640px"
|
||||||
|
class="image"
|
||||||
|
@loaded="imageLoaded = true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<client-only>
|
<client-only>
|
||||||
@ -207,6 +216,11 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
imageLoaded: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
user: 'auth/user',
|
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 {
|
.post-user-row {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@ -335,7 +361,12 @@ export default {
|
|||||||
|
|
||||||
.image-placeholder {
|
.image-placeholder {
|
||||||
width: 100%;
|
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 {
|
> .image {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@ -1,9 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<img :src="image.url" :sizes="sizes" :srcset="srcset" />
|
<img
|
||||||
|
:src="image.url"
|
||||||
|
:sizes="sizes"
|
||||||
|
:srcset="srcset"
|
||||||
|
:class="{ 'responsive-image--loaded': loaded }"
|
||||||
|
class="responsive-image"
|
||||||
|
loading="lazy"
|
||||||
|
fetchpriority="low"
|
||||||
|
@load="onLoad"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
name: 'ResponsiveImage',
|
||||||
|
emits: ['loaded'],
|
||||||
props: {
|
props: {
|
||||||
image: {
|
image: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -14,11 +25,36 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loaded: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
srcset() {
|
srcset() {
|
||||||
const { w320, w640, w1024 } = this.image
|
const { w320, w640, w1024 } = this.image
|
||||||
return `${w320} 320w, ${w640} 640w, ${w1024} 1024w`
|
return `${w320} 320w, ${w640} 640w, ${w1024} 1024w`
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.$el.complete && this.$el.naturalWidth > 0) this.onLoad()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onLoad() {
|
||||||
|
this.loaded = true
|
||||||
|
this.$emit('loaded')
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.responsive-image {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
|
||||||
|
&--loaded {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -190,7 +190,9 @@ exports[`UserTeaser given an user user is disabled current user is a moderator r
|
|||||||
|
|
||||||
<img
|
<img
|
||||||
alt="Tilda Swinton"
|
alt="Tilda Swinton"
|
||||||
class="image"
|
class="responsive-image image"
|
||||||
|
fetchpriority="low"
|
||||||
|
loading="lazy"
|
||||||
sizes="320px"
|
sizes="320px"
|
||||||
src="/avatars/tilda-swinton"
|
src="/avatars/tilda-swinton"
|
||||||
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
||||||
@ -321,7 +323,9 @@ exports[`UserTeaser given an user with linkToProfile, on desktop renders 1`] = `
|
|||||||
|
|
||||||
<img
|
<img
|
||||||
alt="Tilda Swinton"
|
alt="Tilda Swinton"
|
||||||
class="image"
|
class="responsive-image image"
|
||||||
|
fetchpriority="low"
|
||||||
|
loading="lazy"
|
||||||
sizes="320px"
|
sizes="320px"
|
||||||
src="/avatars/tilda-swinton"
|
src="/avatars/tilda-swinton"
|
||||||
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
||||||
@ -411,7 +415,9 @@ exports[`UserTeaser given an user with linkToProfile, on desktop when hovering t
|
|||||||
|
|
||||||
<img
|
<img
|
||||||
alt="Tilda Swinton"
|
alt="Tilda Swinton"
|
||||||
class="image"
|
class="responsive-image image"
|
||||||
|
fetchpriority="low"
|
||||||
|
loading="lazy"
|
||||||
sizes="320px"
|
sizes="320px"
|
||||||
src="/avatars/tilda-swinton"
|
src="/avatars/tilda-swinton"
|
||||||
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
||||||
@ -501,7 +507,9 @@ exports[`UserTeaser given an user with linkToProfile, on touch screen renders 1`
|
|||||||
|
|
||||||
<img
|
<img
|
||||||
alt="Tilda Swinton"
|
alt="Tilda Swinton"
|
||||||
class="image"
|
class="responsive-image image"
|
||||||
|
fetchpriority="low"
|
||||||
|
loading="lazy"
|
||||||
sizes="320px"
|
sizes="320px"
|
||||||
src="/avatars/tilda-swinton"
|
src="/avatars/tilda-swinton"
|
||||||
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
||||||
@ -592,7 +600,9 @@ exports[`UserTeaser given an user with linkToProfile, on touch screen when click
|
|||||||
|
|
||||||
<img
|
<img
|
||||||
alt="Tilda Swinton"
|
alt="Tilda Swinton"
|
||||||
class="image"
|
class="responsive-image image"
|
||||||
|
fetchpriority="low"
|
||||||
|
loading="lazy"
|
||||||
sizes="320px"
|
sizes="320px"
|
||||||
src="/avatars/tilda-swinton"
|
src="/avatars/tilda-swinton"
|
||||||
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
||||||
@ -687,7 +697,9 @@ exports[`UserTeaser given an user without linkToProfile, on desktop renders 1`]
|
|||||||
|
|
||||||
<img
|
<img
|
||||||
alt="Tilda Swinton"
|
alt="Tilda Swinton"
|
||||||
class="image"
|
class="responsive-image image"
|
||||||
|
fetchpriority="low"
|
||||||
|
loading="lazy"
|
||||||
sizes="320px"
|
sizes="320px"
|
||||||
src="/avatars/tilda-swinton"
|
src="/avatars/tilda-swinton"
|
||||||
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
||||||
@ -777,7 +789,9 @@ exports[`UserTeaser given an user without linkToProfile, on desktop when hoverin
|
|||||||
|
|
||||||
<img
|
<img
|
||||||
alt="Tilda Swinton"
|
alt="Tilda Swinton"
|
||||||
class="image"
|
class="responsive-image image"
|
||||||
|
fetchpriority="low"
|
||||||
|
loading="lazy"
|
||||||
sizes="320px"
|
sizes="320px"
|
||||||
src="/avatars/tilda-swinton"
|
src="/avatars/tilda-swinton"
|
||||||
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
||||||
@ -867,7 +881,9 @@ exports[`UserTeaser given an user without linkToProfile, on desktop when hoverin
|
|||||||
|
|
||||||
<img
|
<img
|
||||||
alt="Tilda Swinton"
|
alt="Tilda Swinton"
|
||||||
class="image"
|
class="responsive-image image"
|
||||||
|
fetchpriority="low"
|
||||||
|
loading="lazy"
|
||||||
sizes="320px"
|
sizes="320px"
|
||||||
src="/avatars/tilda-swinton"
|
src="/avatars/tilda-swinton"
|
||||||
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
||||||
@ -957,7 +973,9 @@ exports[`UserTeaser given an user without linkToProfile, on touch screen renders
|
|||||||
|
|
||||||
<img
|
<img
|
||||||
alt="Tilda Swinton"
|
alt="Tilda Swinton"
|
||||||
class="image"
|
class="responsive-image image"
|
||||||
|
fetchpriority="low"
|
||||||
|
loading="lazy"
|
||||||
sizes="320px"
|
sizes="320px"
|
||||||
src="/avatars/tilda-swinton"
|
src="/avatars/tilda-swinton"
|
||||||
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
||||||
@ -1048,7 +1066,9 @@ exports[`UserTeaser given an user without linkToProfile, on touch screen when cl
|
|||||||
|
|
||||||
<img
|
<img
|
||||||
alt="Tilda Swinton"
|
alt="Tilda Swinton"
|
||||||
class="image"
|
class="responsive-image image"
|
||||||
|
fetchpriority="low"
|
||||||
|
loading="lazy"
|
||||||
sizes="320px"
|
sizes="320px"
|
||||||
src="/avatars/tilda-swinton"
|
src="/avatars/tilda-swinton"
|
||||||
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
||||||
@ -1143,7 +1163,9 @@ exports[`UserTeaser given an user without linkToProfile, on touch screen when cl
|
|||||||
|
|
||||||
<img
|
<img
|
||||||
alt="Tilda Swinton"
|
alt="Tilda Swinton"
|
||||||
class="image"
|
class="responsive-image image"
|
||||||
|
fetchpriority="low"
|
||||||
|
loading="lazy"
|
||||||
sizes="320px"
|
sizes="320px"
|
||||||
src="/avatars/tilda-swinton"
|
src="/avatars/tilda-swinton"
|
||||||
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
srcset="/avatars/tilda-swinton-w320 320w, /avatars/tilda-swinton-w640 640w, /avatars/tilda-swinton-w1024 1024w"
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export const isTouchDevice = () =>
|
export const isTouchDevice = () =>
|
||||||
'ontouchstart' in window || navigator.MaxTouchPoints > 0 || navigator.msMaxTouchPoints > 0
|
typeof window !== 'undefined' &&
|
||||||
|
('ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0)
|
||||||
|
|||||||
@ -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()
|
const lang = i18n.locale().toUpperCase()
|
||||||
return gql`
|
return gql`
|
||||||
${user}
|
${user}
|
||||||
|
|||||||
@ -239,6 +239,12 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
manifest,
|
manifest,
|
||||||
|
|
||||||
|
render: {
|
||||||
|
// Generate preload hints for critical JS/CSS/font assets
|
||||||
|
resourceHints: true,
|
||||||
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
** Build configuration
|
** Build configuration
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -344,7 +344,7 @@ export default {
|
|||||||
},
|
},
|
||||||
Post: {
|
Post: {
|
||||||
query() {
|
query() {
|
||||||
return filterPosts(this.$i18n)
|
return filterPosts()
|
||||||
},
|
},
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -66,7 +66,7 @@ import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css'
|
|||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import { profileUserQuery, mapUserQuery } from '~/graphql/User'
|
import { profileUserQuery, mapUserQuery } from '~/graphql/User'
|
||||||
import { groupQuery } from '~/graphql/groups'
|
import { groupQuery } from '~/graphql/groups'
|
||||||
import { filterPosts } from '~/graphql/PostQuery.js'
|
import { filterMapPosts } from '~/graphql/PostQuery.js'
|
||||||
import mobile from '~/mixins/mobile'
|
import mobile from '~/mixins/mobile'
|
||||||
import Empty from '~/components/Empty/Empty'
|
import Empty from '~/components/Empty/Empty'
|
||||||
import MapStylesButtons from '~/components/Map/MapStylesButtons'
|
import MapStylesButtons from '~/components/Map/MapStylesButtons'
|
||||||
@ -542,7 +542,7 @@ export default {
|
|||||||
},
|
},
|
||||||
Post: {
|
Post: {
|
||||||
query() {
|
query() {
|
||||||
return filterPosts(this.$i18n)
|
return filterMapPosts(this.$i18n)
|
||||||
},
|
},
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user