Last active
August 3, 2018 07:14
-
-
Save denisfl/63435039f58f3519a297a15723f65ad5 to your computer and use it in GitHub Desktop.
Vue code example: build grid like Yandex.Zen
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div class="item" :class="itemClasses" :data-id="item.id"> | |
<div class="item-container"> | |
<div class="item-cover"> | |
<div class="item-cover-thumb" :style="thumbUrl"></div> | |
<div class="item-cover-mask"></div> | |
</div> | |
<div class="item-content"> | |
<div class="item-title" :class="titleClasses"> | |
<a class="item-link" :href="item.url" v-if="item.url && !fullArticle"> | |
<span class="item-link-text">{{ item.title }}</span> | |
<span v-if="readMore" class="item-readMore">Читать далее</span> | |
</a> | |
<span v-else class="item-link-text">{{ item.title }}</span> | |
</div> | |
</div> | |
</div> | |
<div class="item-body" v-if="item.body && fullArticle"> | |
<div v-html="item.body"></div> | |
</div> | |
</div> | |
</template> | |
<script> | |
export default { | |
props: { | |
item: { | |
type: Object, | |
required: true | |
}, | |
size: { | |
type: String | |
}, | |
theme: { | |
type: String | |
}, | |
readMore: { | |
type: Boolean, | |
default: false | |
}, | |
fullArticle: { | |
type: Boolean, | |
default: false | |
} | |
}, | |
computed: { | |
itemClasses () { | |
let size = this.size ? `-size-${this.size}` : '' | |
return [ size, `-theme-${this.theme}`, {'has-readMore': this.readMore} ] | |
}, | |
titleClasses () { | |
return { '-font-lg': this.readMore || this.fullArticle } | |
}, | |
thumbUrl () { | |
if (this.size === 'm' || this.size === 'l' || this.fullArticle) { | |
return { 'background-image': `url(${this.item.image_rectangle})` } | |
} else { | |
return { 'background-image': `url(${this.item.image})` } | |
} | |
} | |
} | |
} | |
</script> | |
<style lang="scss"> | |
.item { | |
padding: 4px; | |
width: 100%; | |
&.-size-s { | |
@media (min-width: 640px) { | |
width: 33.333%; | |
} | |
} | |
&.-size-m { | |
@media (min-width: 500px) { | |
width: 50%; | |
} | |
} | |
&.-size-l { | |
@media (min-width: 640px) { | |
width: 66.667%; | |
} | |
} | |
&-container { | |
height: 320px; | |
position: relative; | |
overflow: hidden; | |
text-align: left; | |
border-radius: 4px; | |
background-color: #fff; | |
} | |
&-link { | |
text-decoration: none; | |
color: inherit; | |
&:after, | |
&:before { | |
content: ""; | |
position: absolute; | |
top: 0; | |
right: 0; | |
bottom: 0; | |
left: 0; | |
} | |
&:before { | |
opacity: 0; | |
transition: opacity 0.1s ease-out; | |
background-color: rgba(0, 0, 0, 0.4); | |
} | |
&:hover { | |
&:before { | |
opacity: 1; | |
} | |
} | |
&-text { | |
position: relative; | |
z-index: 2; | |
} | |
} | |
&-cover { | |
position: absolute; | |
top: 0; | |
bottom: 0; | |
left: 0; | |
right: 0; | |
&-thumb { | |
position: absolute; | |
top: 0; | |
right: 0; | |
background-size: contain; | |
background-repeat: no-repeat; | |
height: 100%; | |
.-size-s &, | |
.-size-m & { | |
left: 0; | |
bottom: 0; | |
} | |
.-size-l & { | |
bottom: 0; | |
left: 0; | |
} | |
} | |
&-mask { | |
position: absolute; | |
top: 0; | |
bottom: 0; | |
left: 0; | |
right: 0; | |
.-theme-black & { | |
background: linear-gradient( | |
to bottom, | |
rgba(255, 255, 255, 0) 0%, | |
rgba(255, 255, 255, 1) 80%, | |
rgba(255, 255, 255, 1) 100% | |
); | |
} | |
.-theme-white & { | |
background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 50%, black 90%, black 100%); | |
} | |
} | |
&-hover { | |
position: absolute; | |
left: 0; | |
right: 0; | |
top: 0; | |
bottom: 0; | |
z-index: 3; | |
opacity: 0; | |
transition: opacity 0.1s ease-out; | |
background-color: rgba(0, 0, 0, 0.4); | |
.item-link:hover & { | |
opacity: 1; | |
} | |
} | |
} | |
&-content { | |
padding-top: 24px; | |
padding-bottom: 24px; | |
padding-left: 28px; | |
padding-right: 24px; | |
height: 100%; | |
max-height: 100%; | |
display: flex; | |
flex-direction: column; | |
justify-content: flex-end; | |
border-radius: 4px; | |
&:after { | |
content: ""; | |
position: absolute; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
} | |
.-size-s &, | |
.-size-m & { | |
&:after { | |
height: 48px; | |
} | |
} | |
.-size-l & { | |
width: 66%; | |
} | |
.has-readMore & { | |
padding-bottom: 60px; | |
width: 100%; | |
} | |
.-theme-black & { | |
color: #444; | |
&:after { | |
background: linear-gradient( | |
to bottom, | |
rgba(255, 255, 255, 0) 0%, | |
rgba(255, 255, 255, 1) 50%, | |
rgba(255, 255, 255, 1) 100% | |
); | |
} | |
} | |
.-theme-white & { | |
color: #fff; | |
&:after { | |
background: linear-gradient( | |
to bottom, | |
rgba(0, 0, 0, 0) 0%, | |
rgba(0, 0, 0, 1) 50%, | |
rgba(0, 0, 0, 1) 100% | |
); | |
} | |
} | |
} | |
&-title { | |
margin-bottom: 8px; | |
font-weight: bold; | |
font-size: 18px; | |
line-height: 22px; | |
} | |
.-font-lg { | |
font-size: 24px; | |
line-height: 28px; | |
} | |
&-body { | |
padding-top: 24px; | |
padding-bottom: 24px; | |
padding-left: 28px; | |
padding-right: 24px; | |
background-color: #fff; | |
font-size: 14px; | |
line-height: 20px; | |
} | |
&-readMore { | |
background-color: #fff; | |
display: block; | |
padding: 10px; | |
position: absolute; | |
right: 14px; | |
bottom: 14px; | |
text-decoration: none; | |
color: #444; | |
border-radius: 4px; | |
width: 174px; | |
text-align: center; | |
z-index: 1; | |
font-size: 18px; | |
line-height: 22px; | |
} | |
} | |
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div class="list"> | |
<Row v-for="row in loadedList" :row="row" :key="row.id" /> | |
</div> | |
</template> | |
<script> | |
import each from 'lodash/each' | |
import slice from 'lodash/slice' | |
import shuffle from 'lodash/shuffle' | |
import flatten from 'lodash/flatten' | |
import throttle from 'lodash/throttle' | |
import take from 'lodash/take' | |
import last from 'lodash/last' | |
import difference from 'lodash/difference' | |
import find from 'lodash/find' | |
import Row from './Row.vue' | |
import { mapState, mapGetters, mapActions } from 'vuex' | |
const GRID = { | |
mobile: ['l', 'm m'], | |
tablet: ['l s', 'm m', 's l'], | |
decktop: ['l s', 's s s', 'm m', 's l'] | |
} | |
export default { | |
data() { | |
return { | |
bottom: false, | |
lastScrollTop: 0, | |
viewdItems: [] | |
} | |
}, | |
computed: { | |
...mapGetters(['getItemsForList', 'getArticleId']), | |
loadedList () { | |
return this.groupItems | |
}, | |
groupItems () { | |
let data = this.getItemsForList | |
const sizes = this.addSize | |
let group = [] | |
let n = 0 | |
let start = 0 | |
let end = 0 | |
let item = {} | |
if (data) { | |
each(sizes, (value) => { | |
n = value.length | |
end = start + n | |
item = { | |
size: value, | |
theme: 'white', | |
items: slice(data, start, end) | |
} | |
if (end < data.length) { | |
group.push(item) | |
start = end + 1 | |
} | |
}) | |
} | |
return group | |
}, | |
addSize () { | |
let sizes = [] | |
const type = GRID[`${this.screenSize()}`] | |
let array = [] | |
for (let i = 0; i < type.length; i++) { | |
array.push(shuffle(type)) | |
} | |
each(flatten(array), (value) => { | |
sizes.push(value.split(' ')) | |
}) | |
return sizes | |
} | |
}, | |
methods: { | |
screenSize() { | |
const width = window.screen.width | |
if (width < 640) { | |
return 'mobile' | |
} else if (width < 960) { | |
return 'tablet' | |
} else { | |
return 'decktop' | |
} | |
}, | |
calcViewed() { | |
const visibleArea = document.documentElement.clientHeight | |
const elementWidth = 328 | |
const viewedRows = Math.round((window.scrollY + visibleArea) / elementWidth) | |
let array = [] | |
let diffArray = [] | |
let data = { | |
ids: [], | |
max: null | |
} | |
const scroll = window.pageYOffset || document.documentElement.scrollTop | |
if (scroll > this.lastScrollTop) { | |
each(take(this.groupItems, viewedRows), (value) => { | |
each(value.items, (element) => { | |
array.push(element.id) | |
}) | |
}) | |
diffArray = difference(array, this.viewdItems) | |
if (diffArray.length > 0) { | |
data.ids = diffArray | |
data.max = find(this.getItemsForList, { 'id': last(diffArray) }).serial_number | |
this.sendViewedItems(data) | |
} | |
this.viewdItems = array | |
} | |
this.lastScrollTop = scroll <= 0 ? 0 : scroll | |
}, | |
sendViewedItems (data) { | |
return fetch('/record', { | |
method: 'POST', | |
mode: 'cors', | |
headers:{ | |
'Access-Control-Allow-Origin':'*' | |
}, | |
body: JSON.stringify(data) | |
}) | |
.catch((error) => { | |
// eslint-disable-next-line | |
console.log(error.message) | |
}) | |
} | |
}, | |
created() { | |
const data = { | |
article: { | |
id: this.getArticleId || null | |
} | |
} | |
this.$store.dispatch('fetchList', data).catch((error) => { | |
// eslint-disable-next-line | |
console.log(`Problem with fetching. ${error.message}`) | |
}) | |
window.addEventListener('scroll', throttle(this.calcViewed, 3000)); | |
}, | |
components: { | |
Row | |
} | |
} | |
</script> | |
<style lang="scss"> | |
.list { | |
margin: auto; | |
padding-left: 8px; | |
padding-right: 8px; | |
display: flex; | |
flex-direction: column; | |
@media (min-width: 840px) { | |
width: 90%; | |
} | |
@media (min-width: 1024px) { | |
width: 66%; | |
} | |
} | |
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div class="row"> | |
<Item v-for="(item, index) in row.items" | |
:item="item" | |
:size="row.size[index]" | |
:theme="row.theme" | |
:key="item.id"/> | |
</div> | |
</template> | |
<script> | |
import Item from './Item.vue' | |
export default { | |
props: { | |
row: { | |
type: Object | |
} | |
}, | |
components: { | |
Item | |
} | |
} | |
</script> | |
<style> | |
.row { | |
display: flex; | |
flex-wrap: wrap; | |
margin: 0 -4px; | |
} | |
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Vue from 'vue' | |
import Vuex from 'vuex' | |
import slice from 'lodash/slice' | |
import take from 'lodash/take' | |
import flattenDeep from 'lodash/flattenDeep' | |
import each from 'lodash/each' | |
Vue.use(Vuex) | |
const URL = '/teasers' | |
export default new Vuex.Store({ | |
state: { | |
list: null, | |
item: null, | |
filterItems: [], | |
details: { | |
count: null, | |
item: { | |
article: { | |
title: null, | |
body: null, | |
cover: null, | |
}, | |
fullArticle: false, | |
readMore: false | |
} | |
} | |
}, | |
getters: { | |
loadingItems(state) { | |
return flattenDeep(state.filterItems) | |
}, | |
getItemsForSidebar(state) { | |
return take(state.list, state.details.count) | |
}, | |
getItemsForList(state) { | |
return slice(state.list, state.details.count) | |
}, | |
getArticle(state) { | |
return state.details.item | |
}, | |
getArticleId(state) { | |
return state.details.item.article.id | |
} | |
}, | |
mutations: { | |
setList(state, data) { | |
let serialNumber = 0 | |
state.list = each(data.markets, (value) => { | |
value.serial_number = serialNumber | |
serialNumber += 1 | |
}) | |
}, | |
setItem(state, data) { | |
state.item = data | |
}, | |
setLoadingSince(state) { | |
state.loadingPosition.start += state.count | |
state.loadingPosition.end += state.count | |
}, | |
sliceItems(state) { | |
state.filterItems.push(slice(state.list, state.loadingPosition.start, state.loadingPosition.end)) | |
}, | |
setArticle(state, data) { | |
state.details.item = data | |
}, | |
setSidebarItemsCount(state, data) { | |
state.details.count = data | |
}, | |
showFullArticle(state) { | |
state.details.item.article.fullArticle = true | |
} | |
}, | |
actions: { | |
fetchList({ commit }, data) { | |
return fetch(URL, { | |
method: 'POST', | |
mode: 'cors', | |
headers:{ | |
'Access-Control-Allow-Origin':'*' | |
}, | |
body: JSON.stringify(data) | |
}) | |
.then(response => response.json()) | |
.then((json) => { | |
commit("setList", json) | |
}) | |
}, | |
setFullArticle({ commit }, data) { | |
commit("setArticle", data) | |
}, | |
updateSidebarItemsCount({ commit }, data) { | |
commit("setSidebarItemsCount", data) | |
}, | |
sendViewedItems(data) { | |
return fetch('/record', { | |
method: 'POST', | |
body: JSON.stringify(data) | |
}) | |
.catch((error) => { | |
console.log(error.message) | |
}) | |
} | |
} | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment