finish implementing card layout
This commit is contained in:
parent
46be8e8629
commit
becae553b8
7 changed files with 157 additions and 39 deletions
18
src/App.vue
18
src/App.vue
|
@ -1,8 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<Navbar/>
|
<Navbar/>
|
||||||
|
<div class="container-fluid px-xl-5 mt-3">
|
||||||
<div class="container mt-2">
|
<div class="row" v-if="layout === 'table'">
|
||||||
|
<div class="col-xl-8 offset-xl-2">
|
||||||
<Table
|
<Table
|
||||||
:columns="['uid', 'description', 'box', 'image']"
|
:columns="['uid', 'description', 'box', 'image']"
|
||||||
:items="loadedItems"
|
:items="loadedItems"
|
||||||
|
@ -10,17 +11,26 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Cards
|
||||||
|
v-if="layout === 'cards'"
|
||||||
|
:columns="['uid', 'description', 'box', 'image']"
|
||||||
|
:items="loadedItems"
|
||||||
|
:keyName="'uid'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Navbar from '@/components/Navbar';
|
import Navbar from '@/components/Navbar';
|
||||||
|
import Cards from '@/components/Cards';
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: 'app',
|
||||||
components: { Navbar, Table },
|
components: { Navbar, Table, Cards },
|
||||||
computed: mapState(['loadedItems'])
|
computed: mapState(['loadedItems', 'layout'])
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
72
src/components/Cards.vue
Normal file
72
src/components/Cards.vue
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
<template>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-3 col-xl-2">
|
||||||
|
<div class="card bg-dark text-light mb-2" id="filters">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title text-info">Sort & Filter</h5>
|
||||||
|
<div class="form-group" v-for="(column, index) in columns" :key="index">
|
||||||
|
<label>{{ column }}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<button
|
||||||
|
:class="[ 'btn', column === sortBy ? 'btn-outline-info' : 'btn-outline-secondary' ]"
|
||||||
|
type="button"
|
||||||
|
@click="toggleSort(column)">
|
||||||
|
<font-awesome-icon :icon="getSortIcon(column)"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="filter"
|
||||||
|
:value="filters[column]"
|
||||||
|
@input="setFilter(column, $event.target.value)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-9 col-xl-8">
|
||||||
|
<transition-group name="card-list" tag="div" class="card-columns">
|
||||||
|
<div class="card-list-item card bg-dark text-light" v-for="item in internalItems" :key="item.uid">
|
||||||
|
<img
|
||||||
|
:src="`https://picsum.photos/id/${item.uid + 50}/200/200`"
|
||||||
|
alt="item"
|
||||||
|
class="card-img-top img-fluid"
|
||||||
|
>
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">{{ item.description }}</h6>
|
||||||
|
<h6 class="card-subtitle text-secondary">uid: {{ item.uid }} box: {{ item.box }}</h6>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DataContainer from '@/mixins/data-container';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Cards',
|
||||||
|
mixins: [DataContainer],
|
||||||
|
methods: {
|
||||||
|
random: () => Math.floor((Math.random() * 500) + 300)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.card-list-item {
|
||||||
|
transition: all 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-list-enter, .card-list-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-list-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -15,6 +15,14 @@
|
||||||
<button type="button" class="btn mx-1 text-nowrap" v-for="(button, index) in buttons" v-bind:key="index" :class="['btn-' + button.color]">
|
<button type="button" class="btn mx-1 text-nowrap" v-for="(button, index) in buttons" v-bind:key="index" :class="['btn-' + button.color]">
|
||||||
<font-awesome-icon :icon="button.icon"/><span class="d-none d-md-inline"> {{ button.title }}</span>
|
<font-awesome-icon :icon="button.icon"/><span class="d-none d-md-inline"> {{ button.title }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<div class="btn-group btn-group-toggle">
|
||||||
|
<button :class="['btn', 'btn-info', { active: layout === 'cards' }]" @click="setLayout('cards')">
|
||||||
|
<font-awesome-icon icon="th"/>
|
||||||
|
</button>
|
||||||
|
<button :class="['btn', 'btn-info', { active: layout === 'table' }]" @click="setLayout('table')">
|
||||||
|
<font-awesome-icon icon="list"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
|
||||||
|
@ -38,7 +46,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapActions } from 'vuex';
|
import { mapState, mapActions, mapMutations } from 'vuex';
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -48,14 +56,14 @@ export default {
|
||||||
buttons: [
|
buttons: [
|
||||||
{ title: 'Add', icon: 'plus', color: 'success' },
|
{ title: 'Add', icon: 'plus', color: 'success' },
|
||||||
{ title: 'Refresh', icon: 'sync-alt', color: 'primary' },
|
{ title: 'Refresh', icon: 'sync-alt', color: 'primary' },
|
||||||
{ title: 'Placeholder', icon: 'cat', color: 'warning' },
|
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['events', 'activeEvent'])
|
...mapState(['events', 'activeEvent', 'layout']),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['changeEvent'])
|
...mapActions(['changeEvent']),
|
||||||
|
...mapMutations(['setLayout'])
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" v-for="(column, index) in columns" :key="index">
|
<th scope="col" v-for="(column, index) in columns" :key="index">
|
||||||
<button class="btn text-light" v-on:click="toggleSort(column)">
|
<button class="btn text-light" @click="toggleSort(column)">
|
||||||
{{ column }}
|
{{ column }}
|
||||||
<span :class="{ 'text-info': column === sortBy }">
|
<span :class="{ 'text-info': column === sortBy }">
|
||||||
<font-awesome-icon :icon="sortIcon(column)"/>
|
<font-awesome-icon :icon="getSortIcon(column)"/>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
|
@ -21,34 +21,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as R from 'ramda';
|
import DataContainer from '@/mixins/data-container';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Table',
|
name: 'Table',
|
||||||
props: ['columns', 'items', 'keyName'],
|
mixins: [DataContainer]
|
||||||
data: (self) => ({
|
|
||||||
sortBy: self.keyName,
|
|
||||||
ascend: true
|
|
||||||
}),
|
|
||||||
computed: {
|
|
||||||
internalItems() {
|
|
||||||
const sortByOrd = R.sortBy(R.prop(this.sortBy));
|
|
||||||
const sorted = sortByOrd(this.items, [this.sortBy]);
|
|
||||||
return this.ascend ? sorted : R.reverse(sorted);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
sortIcon(column) {
|
|
||||||
if (column !== this.sortBy) return 'sort';
|
|
||||||
if (this.ascend) return 'sort-up';
|
|
||||||
return 'sort-down';
|
|
||||||
},
|
|
||||||
toggleSort(column) {
|
|
||||||
if (column === this.sortBy)
|
|
||||||
this.ascend = !this.ascend;
|
|
||||||
this.sortBy = column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,10 @@ import 'bootstrap/dist/js/bootstrap.min.js';
|
||||||
|
|
||||||
// fontawesome
|
// fontawesome
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||||
import { faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, faSortUp, faSortDown } from '@fortawesome/free-solid-svg-icons';
|
import { faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, faSortUp, faSortDown, faTh, faList } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
|
|
||||||
library.add(faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, faSortUp, faSortDown);
|
library.add(faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, faSortUp, faSortDown, faTh, faList);
|
||||||
|
|
||||||
Vue.component('font-awesome-icon', FontAwesomeIcon);
|
Vue.component('font-awesome-icon', FontAwesomeIcon);
|
||||||
|
|
||||||
|
|
39
src/mixins/data-container.js
Normal file
39
src/mixins/data-container.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import * as R from 'ramda';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['columns', 'items', 'keyName'],
|
||||||
|
data: (self) => ({
|
||||||
|
sortBy: self.keyName,
|
||||||
|
ascend: true,
|
||||||
|
filters: R.fromPairs(self.columns.map(column => [column, '']))
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
internalItems() {
|
||||||
|
const filtered = this.items.filter(item => this.columns
|
||||||
|
.map(column => {
|
||||||
|
const field = item[column] + '';
|
||||||
|
const filter = this.filters[column];
|
||||||
|
return field.includes(filter);
|
||||||
|
}).reduce((acc, nxt) => acc && nxt, true)
|
||||||
|
);
|
||||||
|
const sortByOrd = R.sortBy(R.prop(this.sortBy));
|
||||||
|
const sorted = sortByOrd(filtered, [this.sortBy]);
|
||||||
|
return this.ascend ? sorted : R.reverse(sorted);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getSortIcon(column) {
|
||||||
|
if (column !== this.sortBy) return 'sort';
|
||||||
|
if (this.ascend) return 'sort-up';
|
||||||
|
return 'sort-down';
|
||||||
|
},
|
||||||
|
toggleSort(column) {
|
||||||
|
if (column === this.sortBy)
|
||||||
|
this.ascend = !this.ascend;
|
||||||
|
this.sortBy = column;
|
||||||
|
},
|
||||||
|
setFilter(column, filter) {
|
||||||
|
this.filters[column] = filter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -7,6 +7,7 @@ export default new Vuex.Store({
|
||||||
state: {
|
state: {
|
||||||
events: ['35c3', 'camp19', '36c3'],
|
events: ['35c3', 'camp19', '36c3'],
|
||||||
activeEvent: '36c3',
|
activeEvent: '36c3',
|
||||||
|
layout: 'cards',
|
||||||
loadedItems: [
|
loadedItems: [
|
||||||
{ uid: 1, description: 'sleeping bag', box: 7, image: 41 },
|
{ uid: 1, description: 'sleeping bag', box: 7, image: 41 },
|
||||||
{ uid: 2, description: 'tent', box: 7, image: 23 },
|
{ uid: 2, description: 'tent', box: 7, image: 23 },
|
||||||
|
@ -14,16 +15,27 @@ export default new Vuex.Store({
|
||||||
{ uid: 4, description: 'power supply black', box: 5, image: 62 },
|
{ uid: 4, description: 'power supply black', box: 5, image: 62 },
|
||||||
{ uid: 5, description: 'pullover yellow "pesthörnchen"', box: 5, image: 84 },
|
{ uid: 5, description: 'pullover yellow "pesthörnchen"', box: 5, image: 84 },
|
||||||
{ uid: 6, description: '"blue black second skin"', box: 6, image: 72 },
|
{ uid: 6, description: '"blue black second skin"', box: 6, image: 72 },
|
||||||
{ uid: 7, description: '"the bike blog" bottle orange', box: 6, image: 71 }
|
{ uid: 7, description: '"the bike blog" bottle orange', box: 6, image: 71 },
|
||||||
|
{ uid: 8, description: 'tshirt guad3c', box: 6, image: 26 },
|
||||||
|
{ uid: 9, description: 'power supply dell', box: 6, image: 74 },
|
||||||
|
{ uid: 10, description: 'blanket green blue', box: 6, image: 25 },
|
||||||
|
{ uid: 11, description: 'cap "ega"', box: 6, image: 71 },
|
||||||
|
{ uid: 12, description: 'water bottle blue "sistema"', box: 3, image: 12 },
|
||||||
|
{ uid: 13, description: 'sun hat black', box: 5, image: 1 },
|
||||||
|
{ uid: 14, description: 'toy truck', box: 6, image: 51 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
changeEvent(state, event) {
|
changeEvent(state, event) {
|
||||||
state.activeEvent = event;
|
state.activeEvent = event;
|
||||||
|
},
|
||||||
|
setLayout(state, layout) {
|
||||||
|
state.layout = layout;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
changeEvent({ commit }, event) {
|
changeEvent({ commit }, event) {
|
||||||
|
// todo: load items from server
|
||||||
// todo: load items from server
|
// todo: load items from server
|
||||||
commit('changeEvent', event);
|
commit('changeEvent', event);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue