<!-- This is the event list component. Ignore the template thing. Just makes it so I don't have to type it in the JS -->
<script id="componentTemplate" text="text/template">
<transition-group
tag="ul"
class="event-card-list"
name="fade-in"
:css="false"
v-on:before-enter="cardBeforeEnter"
v-on:enter="cardEnter"
v-on:leave="cardLeave"
appear>
<li v-for="(item, index) in filteredList" :key="item.title" :data-index="index">
<v-card class="event-card">
<v-layout row>
<img :src="item.pic">
<v-layout column justify-space-between style="padding: 0.8em 1.3em; max-width: 390px;">
<div>
<h1 class="name">{{ item.title }}</h1>
<h3 class="date">{{ item.date }}</h3>
</div>
<div>
<p class="desc">{{ item.desc }}</p>
<div class="location">
<v-icon v-if="item.address">location_on</v-icon>
{{ item.address }}
</div>
</div>
<div class="date-ribbon">
<h2>{{ item.month }}</h2>
<h1>{{ item.day }}</h1>
</div>
</v-layout>
</v-layout>
</v-card>
</li>
</transition-group>
</script>
<!-- Above is the event list component. Below you can ignore -->
<v-app id="app">
<v-toolbar color="red">
<v-toolbar-title class="white--text">
Vue Events Card List by @<a class="white--text" style="border-bottom: 2px solid #fff; padding-bottom: 1px">NoahBres</a>
</v-toolbar-title>
<v-spacer></v-spacer>
<a href="https://twitter.com/intent/tweet?text=Check%20out%20this%20cool%20event%20card%20list%20(made%20in%20Vue)%20by%20%40NoahBres&url=https%3A%2F%2Fnoahbres.github.io%2Fblog%2Fbonfire-devlog-3-how-to-make-an-events-card-list" target="_blank" rel="noopener" style="text-decoration: none;">
<v-btn icon dark>
<v-icon>share</v-icon>
</v-btn>
</a>
</v-toolbar>
<v-content style="background: #eee">
<v-container>
<v-layout justify-center>
<v-layout class="wrapme" column align-center justify-center>
<div :class="['search-bar', searchIsFocused ? 'elevation-6' : 'elevation-3']">
<input placeholder="Search"
v-on:focus="searchFocus()"
v-on:blur="searchUnfocus()"
type="text"
name="search"
v-model="filter.search">
</div>
<v-layout align-center justify-space-between row style="width: 100%;">
<div class="upcoming-events-filter-group">
<input type="radio" id="importantSelect" name="important-select" value="important" v-model="eventsUpcomingFilter" @change="upcomingFilterChange()">
<label for="importantSelect">Important</label>
<input type="radio" id="upcomingSelect" name="upcoming-select" value="upcoming" v-model="eventsUpcomingFilter" @change="upcomingFilterChange()">
<label for="upcomingSelect">Upcoming</label>
<input type="radio" id="finishedSelect" name="finished-select" value="finished" v-model="eventsUpcomingFilter" @change="upcomingFilterChange()">
<label for="finishedSelect">Finished</label>
<div class="underline"></div>
</div>
<v-btn flat style="align-self: flex-end; color: #9E9E9E; margin-right: 1.4em">
<span style="padding-right: 0.4em;">Filter</span>
<v-icon>filter_list</v-icon>
</v-btn>
</v-layout>
<!-- CUSTOM EVENT LIST COMPONENT -->
<event-list
:filter-upcoming="filter.upcoming"
:filter-important="filter.important"
:filter-search="filter.search" />
<!-- THE THING ABOVE IS THE CUSTOM EVENT LIST COMPONENT -->
</v-layout>
</v-layout>
</v-container>
</v-content>
</v-app>
/* Event Card List CSS */
.event-card-list {
margin-top: 4em;
}
.event-card-list li {
list-style: none;
margin: 2em 0;
}
.event-card {
overflow: hidden;
width: 630px;
border-radius: 0.3em;
}
.event-card img {
width: 240px;
height: 200px;
object-fit: cover;
}
.event-card .name {
font-size: 2.3em;
font-weight: 400;
}
.event-card .name a {
text-decoration: none;
/*color: #212121;*/
}
.event-card .date {
font-size: 1.4em;
font-weight: 400;
color: #6D6D6D;
}
.event-card .location {
font-size: 1em;
color: #757575;
}
.event-card .location i {
font-size: 1.1em;
padding-right: 0.3em;
margin-bottom: 0.085em;
}
.event-card .desc {
margin-bottom: 0.2em;
font-size: 1.16em;
padding-left: 0.1em;
}
.event-card .date-ribbon {
position: absolute;
top: 0;
left: 2em;
background: #FE453E;
color: #fff;
padding: 0.2em 1em;
padding-bottom: 0;
border-radius: 0;
}
.event-card .date-ribbon::before, .event-card .date-ribbon::after {
content: '';
position: absolute;
top: 100%;
width: 50%;
height: 30px;
}
.event-card .date-ribbon::before {
left: 0;
border-left:solid 2em #FE453E;
border-top: solid 15px #FE453E;
border-bottom: solid 15px transparent;
border-right: solid 2em transparent;
}
.event-card .date-ribbon::after {
right: 0;
border-right:solid 2em #FE453E;
border-top: solid 15px #FE453E;
border-bottom: solid 15px transparent;
border-left: solid 2em transparent;
}
.event-card .date-ribbon h2 {
font-weight: 500;
font-size: 1.15em;
letter-spacing: 0.07em;
text-align: center;
}
.event-card .date-ribbon h1 {
text-align: center;
font-weight: 400;
font-size: 2.45em;
margin-top: -0.09em;
line-height: 1em;
}
/* Below is page css. Not event card */
.wrapme {
width: 70%;
max-width: 650px;
}
.search-bar {
margin-top: 5em;
background: #fff;
padding: 1em;
width: 100%;
border-radius: 10em;
transition: box-shadow 300ms ease;
}
.search-bar input {
width: 100%;
border-style: none;
color: inherit;
background-color: transparent;
padding-left: 1em;
font-size: 1.3em;
}
.search-bar input:focus {
outline: none;
}
.upcoming-events-filter-group {
padding: 0 2.4em;
position: relative;
display: inline-block;
}
.upcoming-events-filter-group input{
visibility: hidden;
opacity: 0;
position: absolute;
top: -999;
left: -999;
}
.upcoming-events-filter-group label {
cursor: pointer;
font-size: 1.3em;
margin: 0 0.3em;
color: #9E9E9E;
transition: color 300ms ease;
}
.upcoming-events-filter-group input:checked + label {
color: #F07077;
}
.upcoming-events-filter-group .underline {
position: absolute;
bottom: -3px;
left: 2.73em;
height: 2px;
width: 6em;
background: #F07077;
transition: 300ms ease;
}
.upcoming-events-filter-group #importantSelect:checked ~ .underline {
left: 2.73em;
width: 5.7em;
}
.upcoming-events-filter-group #upcomingSelect:checked ~ .underline {
left: 9.45em;
width: 6em;
}
.upcoming-events-filter-group #finishedSelect:checked ~ .underline {
left: calc(100% - 2.7em - 5em);
width: 5em;
}
Vue.component('event-list', {
props: ['filterUpcoming', 'filterImportant', 'filterSearch'],
data: () => ({
eventList: [
{
title: 'Scuba Merit Badge',
date: 'August 28 | 8am - 3pm',
desc: 'Earn your scuba diving merit badge. Pre-req: Requirement 1a, 2b, 4ab',
address: '503 Harbor Blvd, Destin, FL',
pic: 'https://images.unsplash.com/photo-1484507175567-a114f764f78b?ixlib=rb-0.3.5&s=abc2cb4d7e6d8aca1e8914c1b5e909a6&auto=format&fit=crop&w=500&q=60',
month: 'Aug',
day: '28',
important: true,
upcoming: true
},
{
title: 'Backpacking Hike',
date: 'June 4th, 2018',
desc: '10mi backpacking hike at Thunder Mountain. Remember to pack properly',
address: 'Thunder Mtn, Disney, FL',
pic: 'https://images.unsplash.com/photo-1467139701929-18c0d27a7516?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=874439394c29dfb8f4b5a794a51a52f2&auto=format&fit=crop&w=750&q=80',
month: 'Jun',
day: '04',
important: false,
upcoming: true
},
{
title: 'Black Forest Camp',
date: 'March 3 - March 5, 2018',
desc: 'Weekend campout in the Black Forest',
address: 'Black Forest, Baden-Württemberg, DE',
pic: 'https://images.unsplash.com/photo-1501703979959-797917eb21c8?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=d4132e8087781addd674e137a9f596dc&auto=format&fit=crop&w=889&q=80',
month: 'Mar',
day: '03',
important: false,
upcoming: true
},
{
title: 'Artic Campout',
date: 'December 14 - 18, 2018',
desc: 'Campout in the artic. Freeze your toes off. See cute penguins.',
address: 'Barrow, Alaska, US',
pic: 'https://images.unsplash.com/photo-1498279898147-67f541d32b6a?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=af428042e69ac5152855548d8b4f7989&auto=format&fit=crop&w=667&q=80',
month: 'Dec',
day: '14',
important: false,
upcoming: false
},
{
title: 'Sailing',
date: 'April 23 | 11am - 7pm',
desc: 'Sail the high seas. Get lost in the Bermuda Triangle.',
address: 'Second star to the right, and straight on till morning',
pic: 'https://images.unsplash.com/photo-1500514966906-fe245eea9344?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=9193225514494f3e830d444d4ae58819&auto=format&fit=crop&w=667&q=80',
month: 'Apr',
day: '23',
important: false,
upcoming: false
}
]
}),
computed: {
filteredList() {
this.filterUpcoming
return this.eventList.filter(e => {
let conditions = [true, true, true];
conditions[0] = e.upcoming == this.filterUpcoming;
if(this.filterImportant)
conditions[1] = e.important == this.filterImportant;
if(this.filterSearch.trim() != '')
conditions[2] = e.title.toLowerCase().includes(this.filterSearch.trim().toLowerCase());
return conditions.every(e => e === true);
});
}
},
methods: {
cardBeforeEnter(el) {
el.style.opacity = 0;
el.style.transform = 'scale(90%)';
el.style.height = 0;
},
cardEnter(el, done) {
let delay = el.dataset.index * 200;
setTimeout(() => {
Velocity(
el,
{ opacity: 1, height: '100%', scale: '100%' },
{ complete: done }
);
}, delay);
},
cardLeave(el, done) {
let delay = el.dataset.index * 200;
setTimeout(() => {
Velocity(
el,
{ opacity: 0, height: 0, scale: '90%' },
{ complete: done }
);
}, delay);
}
},
created() {
},
template: document.getElementById('componentTemplate').innerHTML
});
new Vue({
el: "#app",
data: () => ({
searchIsFocused: false,
eventsUpcomingFilter: 'important',
filter: {
upcoming: true,
important: true,
search: ''
}
}),
methods: {
searchFocus() {
this.searchIsFocused = true;
},
searchUnfocus() {
this.searchIsFocused = false;
},
upcomingFilterChange() {
switch(this.eventsUpcomingFilter) {
case 'important':
this.filter.upcoming = true;
this.filter.important = true;
break;
case 'upcoming':
this.filter.upcoming = true;
this.filter.important = false;
break;
case 'finished':
this.filter.upcoming = false;
this.filter.important = false;
break;
}
}
}
});
View Compiled