Skip to content

Instantly share code, notes, and snippets.

@denisfl
Last active August 3, 2018 07:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save denisfl/63435039f58f3519a297a15723f65ad5 to your computer and use it in GitHub Desktop.
Save denisfl/63435039f58f3519a297a15723f65ad5 to your computer and use it in GitHub Desktop.
Vue code example: build grid like Yandex.Zen
<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>
<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>
<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>
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