<!-- 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
Run Pen

External CSS

  1. https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons
  2. https://cdnjs.cloudflare.com/ajax/libs/vuetify/1.1.1/vuetify.css

External JavaScript

  1. https://unpkg.com/babel-polyfill/dist/polyfill.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/vuetify/1.1.1/vuetify.js
  4. https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js