make indentation consistent

This commit is contained in:
j3d1 2023-11-27 01:14:52 +01:00
parent d52575aa42
commit 9f63414ba2
27 changed files with 858 additions and 804 deletions

View file

@ -1,7 +1,7 @@
root = true root = true
[*.js] [*.js]
indent_size = 2 indent_size = 4
[*.vue] [*.vue]
indent_size = 2 indent_size = 4

View file

@ -17,8 +17,7 @@ module.exports = {
'rules': { 'rules': {
"no-console": "off", "no-console": "off",
'indent': [ 'indent': [
'error', 'error', 4
2
], ],
'linebreak-style': [ 'linebreak-style': [
'off', 'off',

View file

@ -1,5 +1,5 @@
module.exports = { module.exports = {
presets: [ presets: [
'@vue/cli-plugin-babel/preset' '@vue/cli-plugin-babel/preset'
] ]
}; };

View file

@ -1,17 +1,18 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>c3cloc</title> <title>c3cloc</title>
</head> </head>
<body> <body>
<noscript> <noscript>
<strong>We're sorry but c3cloc doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> <strong>We're sorry but c3cloc doesn't work properly without JavaScript enabled. Please enable it to
</noscript> continue.</strong>
<div id="app"></div> </noscript>
<!-- built files will be auto injected --> <div id="app"></div>
</body> <!-- built files will be auto injected -->
</body>
</html> </html>

View file

@ -1,15 +1,16 @@
<template> <template>
<div id="app"> <div id="app">
<AddItemModal v-if="addModalOpen" @close="closeAddModal()" isModal="true"/> <AddItemModal v-if="addModalOpen" @close="closeAddModal()" isModal="true"/>
<Navbar @addClicked="openAddModal()"/> <Navbar @addClicked="openAddModal()"/>
<router-view/> <router-view/>
<div aria-live="polite" aria-atomic="true" <div aria-live="polite" aria-atomic="true"
class="d-flex justify-content-end align-items-start fixed-top mx-1 my-5 py-3" class="d-flex justify-content-end align-items-start fixed-top mx-1 my-5 py-3"
style="min-height: 200px; z-index: 100000; pointer-events: none"> style="min-height: 200px; z-index: 100000; pointer-events: none">
<Toast v-for="toast in toasts" :key="toast" :title="toast.title" :message="toast.message" :color="toast.color" <Toast v-for="toast in toasts" :key="toast" :title="toast.title" :message="toast.message"
@close="removeToast(toast.key)" style="pointer-events: auto"/> :color="toast.color"
@close="removeToast(toast.key)" style="pointer-events: auto"/>
</div>
</div> </div>
</div>
</template> </template>
<script> <script>
@ -19,55 +20,55 @@ import Toast from './components/Toast';
import {mapState, mapMutations} from 'vuex'; import {mapState, mapMutations} from 'vuex';
export default { export default {
name: 'app', name: 'app',
components: {Toast, Navbar, AddItemModal}, components: {Toast, Navbar, AddItemModal},
computed: mapState(['loadedItems', 'layout', 'toasts']), computed: mapState(['loadedItems', 'layout', 'toasts']),
data: () => ({ data: () => ({
addModalOpen: false addModalOpen: false
}), }),
methods: { methods: {
...mapMutations(['removeToast', 'createToast']), ...mapMutations(['removeToast', 'createToast']),
openAddModal() { openAddModal() {
this.addModalOpen = true; this.addModalOpen = true;
},
closeAddModal() {
this.addModalOpen = false;
}
}, },
closeAddModal() { created: function () {
this.addModalOpen = false; this.notify_socket = new WebSocket('wss://' + window.location.host + '/ws/notify/');
this.notify_socket.onmessage = (e) => {
let data = JSON.parse(e.data);
console.log(data, e.data);
};
this.notify_socket.onopen = (e) => {
this.createToast({
title: "Connection established",
message: JSON.stringify(e),
color: "success"
});
};
this.notify_socket.onclose = (e) => {
this.createToast({
title: "Connection closed",
message: JSON.stringify(e),
color: "danger"
});
};
this.notify_socket.onerror = (e) => {
this.createToast({
title: "Connection error",
message: JSON.stringify(e),
color: "danger"
});
};
} }
},
created: function () {
this.notify_socket = new WebSocket('wss://' + window.location.host + '/ws/notify/');
this.notify_socket.onmessage = (e) => {
let data = JSON.parse(e.data);
console.log(data, e.data);
};
this.notify_socket.onopen = (e) => {
this.createToast({
title: "Connection established",
message: JSON.stringify(e),
color: "success"
});
};
this.notify_socket.onclose = (e) => {
this.createToast({
title: "Connection closed",
message: JSON.stringify(e),
color: "danger"
});
};
this.notify_socket.onerror = (e) => {
this.createToast({
title: "Connection error",
message: JSON.stringify(e),
color: "danger"
});
};
}
}; };
</script> </script>
<style> <style>
body, html, #app { body, html, #app {
background: #000; background: #000;
} }
</style> </style>

View file

@ -1,15 +1,15 @@
<template> <template>
<div> <div>
<Modal v-if="isModal" title="Add Item" @close="$emit('close')"> <Modal v-if="isModal" title="Add Item" @close="$emit('close')">
<template #body> <template #body>
<EditItem :item="item"/> <EditItem :item="item"/>
</template> </template>
<template #buttons> <template #buttons>
<button type="button" class="btn btn-secondary" @click="$emit('close')">Cancel</button> <button type="button" class="btn btn-secondary" @click="$emit('close')">Cancel</button>
<button type="button" class="btn btn-success" @click="saveNewItem()">Save new Item</button> <button type="button" class="btn btn-success" @click="saveNewItem()">Save new Item</button>
</template> </template>
</Modal> </Modal>
</div> </div>
</template> </template>
<script> <script>
@ -17,22 +17,22 @@ import Modal from '@/components/Modal';
import EditItem from '@/components/EditItem'; import EditItem from '@/components/EditItem';
export default { export default {
name: 'AddItemModal', name: 'AddItemModal',
components: { Modal, EditItem }, components: {Modal, EditItem},
props: ['isModal'], props: ['isModal'],
data: () => ({ data: () => ({
item: {} item: {}
}), }),
created() { created() {
this.item = {box: this.$store.state.lastUsed.box || '', cid: this.$store.state.lastUsed.cid || ''}; this.item = {box: this.$store.state.lastUsed.box || '', cid: this.$store.state.lastUsed.cid || ''};
}, },
methods: { methods: {
saveNewItem() { saveNewItem() {
this.$store.dispatch('postItem', this.item).then(() => { this.$store.dispatch('postItem', this.item).then(() => {
this.$emit('close'); this.$emit('close');
}); });
}
} }
}
}; };
</script> </script>

View file

@ -22,27 +22,27 @@
:value="filters[column]" :value="filters[column]"
@input="changeFilter(column, $event.target.value)" @input="changeFilter(column, $event.target.value)"
> >
<!-- <input @change="someHandler"> --> <!-- <input @change="someHandler"> -->
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-9 col-xl-8"> <div class="col-lg-9 col-xl-8">
<div class="row"> <div class="row">
<div <div
class="mb-4 col-lg-4 col-xl-3" class="mb-4 col-lg-4 col-xl-3"
v-for="item in internalItems" v-for="item in internalItems"
:key="item[keyName]" :key="item[keyName]"
> >
<div <div
class="card-list-item card bg-dark text-light" class="card-list-item card bg-dark text-light"
@click="$emit('itemActivated', item)" @click="$emit('itemActivated', item)"
> >
<slot v-bind:item="item"/> <slot v-bind:item="item"/>
</div> </div>
</div>
</div> </div>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -52,17 +52,23 @@ import DataContainer from '@/mixins/data-container';
import router from '../router'; import router from '../router';
export default { export default {
name: 'Cards', name: 'Cards',
mixins: [DataContainer], mixins: [DataContainer],
created() { created() {
this.columns.map(e => ({k: e, v: this.$store.getters.getFilters[e]})).filter(e => e.v).forEach(e => this.setFilter(e.k, e.v)); this.columns.map(e => ({
}, k: e,
methods: { v: this.$store.getters.getFilters[e]
changeFilter(col, val) { })).filter(e => e.v).forEach(e => this.setFilter(e.k, e.v));
this.setFilter(col, val); },
let newquery = Object.entries({...this.$store.getters.getFilters, [col]: val}).reduce((a,[k,v]) => (v ? {...a, [k]:v} : a), {}); methods: {
router.push({query: newquery}); changeFilter(col, val) {
this.setFilter(col, val);
let newquery = Object.entries({
...this.$store.getters.getFilters,
[col]: val
}).reduce((a, [k, v]) => (v ? {...a, [k]: v} : a), {});
router.push({query: newquery});
},
}, },
},
}; };
</script> </script>

View file

@ -25,23 +25,23 @@
<script> <script>
import InputString from './inputs/InputString'; import InputString from './inputs/InputString';
import InputCombo from './inputs/InputCombo'; import InputCombo from './inputs/InputCombo';
import { mapGetters } from 'vuex'; import {mapGetters} from 'vuex';
import InputPhoto from './inputs/InputPhoto'; import InputPhoto from './inputs/InputPhoto';
export default { export default {
name: 'EditItem', name: 'EditItem',
components: {InputPhoto, InputCombo, InputString }, components: {InputPhoto, InputCombo, InputString},
props: ['item'], props: ['item'],
computed: { computed: {
...mapGetters(['getBoxes']), ...mapGetters(['getBoxes']),
boxes({ getBoxes }) { boxes({getBoxes}) {
return getBoxes.map(obj => ({cid: obj.cid, box: obj.name})); return getBoxes.map(obj => ({cid: obj.cid, box: obj.name}));
}
},
methods: {
storeImage(image) {
this.item.dataImage = image;
}
} }
},
methods: {
storeImage(image) {
this.item.dataImage = image;
}
}
}; };
</script> </script>

View file

@ -19,18 +19,18 @@ import Modal from '@/components/Modal';
import config from '../config'; import config from '../config';
export default { export default {
name: 'Lightbox', name: 'Lightbox',
components: { Modal }, components: {Modal},
props: ['file'], props: ['file'],
data: ()=>({ data: () => ({
baseUrl: config.service.url, baseUrl: config.service.url,
}), }),
}; };
</script> </script>
<style> <style>
#lightbox-image { #lightbox-image {
max-height: 75vh; max-height: 75vh;
object-fit: contain; object-fit: contain;
} }
</style> </style>

View file

@ -29,47 +29,47 @@
<script> <script>
export default { export default {
name: 'Modal', name: 'Modal',
props: ['title'], props: ['title'],
mounted() { mounted() {
this.$el.focus(); this.$el.focus();
} }
}; };
</script> </script>
<style> <style>
.modal { .modal {
background-color: rgba(0,0,0,0.4); /* Transparent dimmed overlay */ background-color: rgba(0, 0, 0, 0.4); /* Transparent dimmed overlay */
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
display: table !important; display: table !important;
} }
.modal.hidden { .modal.hidden {
display: none; display: none;
} }
.modal .container { .modal .container {
display: table-cell; display: table-cell;
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
width: 200px; width: 200px;
} }
.modal .body { .modal .body {
box-shadow: 5px 10px #888888; box-shadow: 5px 10px #888888;
display: inline-block; display: inline-block;
background-color: white; background-color: white;
border: 1px solid black; border: 1px solid black;
padding: 10px; padding: 10px;
} }
/* Hacky Fix, for this Issue: https://hannover.ccc.de/gitlab/c3lf/lffrontend/issues/26 */ /* Hacky Fix, for this Issue: https://hannover.ccc.de/gitlab/c3lf/lffrontend/issues/26 */
/*.modal-body { /*.modal-body {
max-height: calc(100vh - 200px); max-height: calc(100vh - 200px);
overflow-y: auto; overflow-y: auto;
}*/ }*/
</style> </style>

View file

@ -3,7 +3,7 @@
<div class="dropdown"> <div class="dropdown">
<button class="btn text-light dropdown-toggle btn-heading" type="button" id="dropdownMenuButton" <button class="btn text-light dropdown-toggle btn-heading" type="button" id="dropdownMenuButton"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{getEventSlug}} {{ getEventSlug }}
</button> </button>
<div class="dropdown-menu bg-dark" aria-labelledby="dropdownMenuButton"> <div class="dropdown-menu bg-dark" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item text-light" href="#" v-for="(event, index) in events" v-bind:key="index" <a class="dropdown-item text-light" href="#" v-for="(event, index) in events" v-bind:key="index"
@ -13,7 +13,8 @@
<div class="custom-control-inline mr-1"> <div class="custom-control-inline mr-1">
<button type="button" class="btn mx-1 text-nowrap btn-success" @click="$emit('addClicked')"> <button type="button" class="btn mx-1 text-nowrap btn-success" @click="$emit('addClicked')">
<font-awesome-icon icon="plus"/><span class="d-none d-md-inline">&nbsp;Add</span> <font-awesome-icon icon="plus"/>
<span class="d-none d-md-inline">&nbsp;Add</span>
</button> </button>
<div class="btn-group btn-group-toggle"> <div class="btn-group btn-group-toggle">
<button :class="['btn', 'btn-info', { active: layout === 'cards' }]" @click="setLayout('cards')"> <button :class="['btn', 'btn-info', { active: layout === 'cards' }]" @click="setLayout('cards')">
@ -32,13 +33,13 @@
<form class="form-inline mt-1 my-lg-auto my-xl-auto w-100 d-inline"> <form class="form-inline mt-1 my-lg-auto my-xl-auto w-100 d-inline">
<input <input
class="form-control w-100" class="form-control w-100"
type="search" type="search"
placeholder="Search" placeholder="Search"
aria-label="Search" aria-label="Search"
v-debounce:500ms="myFunc" v-debounce:500ms="myFunc"
@input="searchEventItems($event.target.value)" @input="searchEventItems($event.target.value)"
disabled disabled
> >
</form> </form>
@ -47,10 +48,11 @@
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<button class="btn nav-link dropdown-toggle" type="button" id="dropdownMenuButton2" <button class="btn nav-link dropdown-toggle" type="button" id="dropdownMenuButton2"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{getActiveView}} {{ getActiveView }}
</button> </button>
<ul class="dropdown-menu bg-dark" aria-labelledby="dropdownMenuButton2"> <ul class="dropdown-menu bg-dark" aria-labelledby="dropdownMenuButton2">
<li class="" v-for="(link, index) in views" v-bind:key="index" :class="{ active: link.path === getActiveView }"> <li class="" v-for="(link, index) in views" v-bind:key="index"
:class="{ active: link.path === getActiveView }">
<a class="nav-link text-nowrap" href="#" @click="changeView(link)">{{ link.title }}</a> <a class="nav-link text-nowrap" href="#" @click="changeView(link)">{{ link.title }}</a>
</li> </li>
</ul> </ul>
@ -66,31 +68,31 @@
</template> </template>
<script> <script>
import { mapState, mapActions, mapMutations, mapGetters } from 'vuex'; import {mapState, mapActions, mapMutations, mapGetters} from 'vuex';
export default { export default {
name: 'Navbar', name: 'Navbar',
data: () => ({ data: () => ({
views: [ views: [
{'title':'items','path':'items'}, {'title': 'items', 'path': 'items'},
{'title':'boxes','path':'boxes'}, {'title': 'boxes', 'path': 'boxes'},
//{'title':'mass-edit','path':'massedit'}, //{'title':'mass-edit','path':'massedit'},
], ],
links: [ links: [
{'title':'howto engel','path':'/howto/'} {'title': 'howto engel', 'path': '/howto/'}
] ]
}), }),
computed: { computed: {
...mapState(['events', 'activeEvent', 'layout']), ...mapState(['events', 'activeEvent', 'layout']),
...mapGetters(['getEventSlug', 'getActiveView']), ...mapGetters(['getEventSlug', 'getActiveView']),
}, },
methods: { methods: {
...mapActions(['changeEvent', 'changeView','searchEventItems']), ...mapActions(['changeEvent', 'changeView', 'searchEventItems']),
...mapMutations(['setLayout']) ...mapMutations(['setLayout'])
} }
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
@import "../scss/navbar.scss"; @import "../scss/navbar.scss";
</style> </style>

View file

@ -1,39 +1,39 @@
<template> <template>
<table class="table table-striped table-dark"> <table class="table table-striped table-dark">
<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">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<button <button
:class="[ 'btn', column === sortBy ? 'btn-outline-info' : 'btn-outline-secondary' ]" :class="[ 'btn', column === sortBy ? 'btn-outline-info' : 'btn-outline-secondary' ]"
@click="toggleSort(column)" @click="toggleSort(column)"
> >
{{ column }} {{ column }}
<span :class="{ 'text-info': column === sortBy }"> <span :class="{ 'text-info': column === sortBy }">
<font-awesome-icon :icon="getSortIcon(column)"/> <font-awesome-icon :icon="getSortIcon(column)"/>
</span> </span>
</button> </button>
</div>
<input
type="text"
class="form-control"
placeholder="filter"
:value="filters[column]"
@input="changeFilter(column, $event.target.value)"
>
</div> </div>
</th> <input
<th></th> type="text"
</tr> class="form-control"
placeholder="filter"
:value="filters[column]"
@input="changeFilter(column, $event.target.value)"
>
</div>
</th>
<th></th>
</tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="item in internalItems" :key="item[keyName]" @click="$emit('itemActivated', item)"> <tr v-for="item in internalItems" :key="item[keyName]" @click="$emit('itemActivated', item)">
<td v-for="(column, index) in columns" :key="index">{{ item[column] }}</td> <td v-for="(column, index) in columns" :key="index">{{ item[column] }}</td>
<td> <td>
<slot v-bind:item="item"/> <slot v-bind:item="item"/>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</template> </template>
@ -43,23 +43,29 @@ import DataContainer from '@/mixins/data-container';
import router from '../router'; import router from '../router';
export default { export default {
name: 'Table', name: 'Table',
mixins: [DataContainer], mixins: [DataContainer],
created() { created() {
this.columns.map(e => ({k: e, v: this.$store.getters.getFilters[e]})).filter(e => e.v).forEach(e => this.setFilter(e.k, e.v)); this.columns.map(e => ({
}, k: e,
methods: { v: this.$store.getters.getFilters[e]
changeFilter(col, val) { })).filter(e => e.v).forEach(e => this.setFilter(e.k, e.v));
this.setFilter(col, val); },
let newquery = Object.entries({...this.$store.getters.getFilters, [col]: val}).reduce((a,[k,v]) => (v ? {...a, [k]:v} : a), {}); methods: {
router.push({query: newquery}); changeFilter(col, val) {
this.setFilter(col, val);
let newquery = Object.entries({
...this.$store.getters.getFilters,
[col]: val
}).reduce((a, [k, v]) => (v ? {...a, [k]: v} : a), {});
router.push({query: newquery});
},
}, },
},
}; };
</script> </script>
<style> <style>
.table-body-move { .table-body-move {
transition: transform 1s; transition: transform 1s;
} }
</style> </style>

View file

@ -14,35 +14,35 @@
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import 'bootstrap/js/dist/toast'; import 'bootstrap/js/dist/toast';
import { DateTime } from 'luxon'; import {DateTime} from 'luxon';
export default { export default {
name: 'Toast', name: 'Toast',
props: ['title', 'message', 'color'], props: ['title', 'message', 'color'],
data: () => ({ data: () => ({
creationTime: DateTime.local(), creationTime: DateTime.local(),
displayTime: 'just now', displayTime: 'just now',
timer: undefined timer: undefined
}), }),
mounted() { mounted() {
const { toast } = this.$refs; const {toast} = this.$refs;
$(toast).toast('show'); $(toast).toast('show');
this.timer = setInterval(this.updateDisplayTime, 1000); this.timer = setInterval(this.updateDisplayTime, 1000);
},
methods: {
close() {
const { toast } = this.$refs;
$(toast).toast('hide');
window.setTimeout(() => {
this.$emit('close');
}, 500);
}, },
updateDisplayTime() { methods: {
this.displayTime = this.creationTime.toRelative(); close() {
const {toast} = this.$refs;
$(toast).toast('hide');
window.setTimeout(() => {
this.$emit('close');
}, 500);
},
updateDisplayTime() {
this.displayTime = this.creationTime.toRelative();
}
},
beforeDestroy() {
clearInterval(this.timer);
} }
},
beforeDestroy() {
clearInterval(this.timer);
}
}; };
</script> </script>

View file

@ -1,22 +1,22 @@
<template> <template>
<div class="input-group-append"> <div class="input-group-append">
<span <span
class="input-group-text" class="input-group-text"
data-toggle="tooltip" data-toggle="tooltip"
data-placement="top" data-placement="top"
:title="type" :title="type"
>{{ type[0] }}</span> >{{ type[0] }}</span>
<button <button
type="button" type="button"
class="btn" class="btn"
:class="'btn-' + color" :class="'btn-' + color"
data-toggle="tooltip" data-toggle="tooltip"
data-placement="top" data-placement="top"
:title="tooltip" :title="tooltip"
> >
<font-awesome-icon :icon="icon"/> <font-awesome-icon :icon="icon"/>
</button> </button>
</div> </div>
</template> </template>
<script> <script>
@ -24,27 +24,27 @@ import $ from 'jquery';
import 'bootstrap/js/dist/tooltip'; import 'bootstrap/js/dist/tooltip';
export default { export default {
name: 'Addon', name: 'Addon',
props: [ 'type', 'isValid' ], props: ['type', 'isValid'],
mounted() { mounted() {
$('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="tooltip"]').tooltip();
},
computed: {
icon() {
if (this.isValid == undefined) return 'pen';
if (this.isValid == false) return 'times';
return 'check';
}, },
color() { computed: {
if (this.isValid == undefined) return 'info'; icon() {
if (this.isValid == false) return 'danger'; if (this.isValid == undefined) return 'pen';
return 'success'; if (this.isValid == false) return 'times';
}, return 'check';
tooltip() { },
if (this.isValid == undefined) return 'no data validation'; color() {
if (this.isValid == false) return 'data invalid'; if (this.isValid == undefined) return 'info';
return 'data valid'; if (this.isValid == false) return 'danger';
return 'success';
},
tooltip() {
if (this.isValid == undefined) return 'no data validation';
if (this.isValid == false) return 'data invalid';
return 'data valid';
}
} }
}
}; };
</script> </script>

View file

@ -7,7 +7,8 @@
class="btn btn-outline-secondary dropdown-toggle" class="btn btn-outline-secondary dropdown-toggle"
type="button" type="button"
data-toggle="dropdown" data-toggle="dropdown"
>Search</button> >Search
</button>
<div class="dropdown-menu"> <div class="dropdown-menu">
<a <a
v-for="(option, index) in sortedOptions" v-for="(option, index) in sortedOptions"
@ -16,20 +17,20 @@
@click="setInternalValue(option)" @click="setInternalValue(option)"
:class="{ active: option == selectedOption }" :class="{ active: option == selectedOption }"
> >
{{ option[nameKey] }} {{ option[nameKey] }}
</a> </a>
</div> </div>
</div> </div>
<input type="text" class="form-control" :id="label" v-model="internalName"> <input type="text" class="form-control" :id="label" v-model="internalName">
<div class="input-group-append"> <div class="input-group-append">
<button <button
class="btn" class="btn"
:class="{ 'btn-info disabled': isValid, 'btn-success': !isValid }" :class="{ 'btn-info disabled': isValid, 'btn-success': !isValid }"
v-if="!isValid" v-if="!isValid"
@click="addOption()" @click="addOption()"
> >
<font-awesome-icon icon="plus"/> <font-awesome-icon icon="plus"/>
</button> </button>
</div> </div>
<Addon type="Combo Box" :is-valid="isValid"/> <Addon type="Combo Box" :is-valid="isValid"/>
</div> </div>
@ -40,37 +41,40 @@
import Addon from './Addon'; import Addon from './Addon';
export default { export default {
name: 'InputCombo', name: 'InputCombo',
components: {Addon}, components: {Addon},
props: [ 'label', 'model', 'nameKey', 'uniqueKey', 'options', 'onOptionAdd' ], props: ['label', 'model', 'nameKey', 'uniqueKey', 'options', 'onOptionAdd'],
data: ({ options, model, nameKey, uniqueKey }) => ({ data: ({options, model, nameKey, uniqueKey}) => ({
internalName: model[nameKey], internalName: model[nameKey],
selectedOption: options.filter(e => e[uniqueKey] == model[uniqueKey])[0], selectedOption: options.filter(e => e[uniqueKey] == model[uniqueKey])[0],
addingOption: false addingOption: false
}), }),
computed: { computed: {
isValid: ({options, nameKey, internalName}) => options.some(e => e[nameKey] == internalName), isValid: ({options, nameKey, internalName}) => options.some(e => e[nameKey] == internalName),
sortedOptions: ({options, nameKey}) => options.sort((a, b) => a[nameKey].localeCompare(b[nameKey], 'en', { numeric: true })), sortedOptions: ({
}, options,
watch: { nameKey
internalName(newValue) { }) => options.sort((a, b) => a[nameKey].localeCompare(b[nameKey], 'en', {numeric: true})),
if (this.isValid) {
if(!this.selectedOption || newValue!=this.selectedOption[this.nameKey]){
this.selectedOption = this.options.filter(e => e[this.nameKey] === newValue)[0];
}
this.model[this.nameKey] = this.selectedOption[this.nameKey];
this.model[this.uniqueKey] = this.selectedOption[this.uniqueKey];
}
}
},
methods: {
setInternalValue(option) {
this.selectedOption = option;
this.internalName = option[this.nameKey];
}, },
addOption() { watch: {
this.onOptionAdd({[this.nameKey]: this.internalName}); internalName(newValue) {
if (this.isValid) {
if (!this.selectedOption || newValue != this.selectedOption[this.nameKey]) {
this.selectedOption = this.options.filter(e => e[this.nameKey] === newValue)[0];
}
this.model[this.nameKey] = this.selectedOption[this.nameKey];
this.model[this.uniqueKey] = this.selectedOption[this.uniqueKey];
}
}
},
methods: {
setInternalValue(option) {
this.selectedOption = option;
this.internalName = option[this.nameKey];
},
addOption() {
this.onOptionAdd({[this.nameKey]: this.internalName});
}
} }
}
}; };
</script> </script>

View file

@ -25,7 +25,7 @@
</label> </label>
<input type="file" id="file" accept="image/png" class="d-none" @change="onFileChange($event)"> <input type="file" id="file" accept="image/png" class="d-none" @change="onFileChange($event)">
<button v-if="!capturing" class="btn my-2 ml-auto btn-secondary" @click="openStream()"> <button v-if="!capturing" class="btn my-2 ml-auto btn-secondary" @click="openStream()">
<font-awesome-icon icon="camera"/> <font-awesome-icon icon="camera"/>
</button> </button>
<div v-if="capturing" class="btn-group my-2 ml-auto"> <div v-if="capturing" class="btn-group my-2 ml-auto">
<button class="btn btn-success" @click="captureVideoImage()"> <button class="btn btn-success" @click="captureVideoImage()">
@ -44,80 +44,84 @@
</template> </template>
<script> <script>
import { mapMutations } from 'vuex'; import {mapMutations} from 'vuex';
export default { export default {
name: 'InputPhoto', name: 'InputPhoto',
props: [ 'model', 'field', 'onCapture' ], props: ['model', 'field', 'onCapture'],
data: () => ({ data: () => ({
capturing: false, capturing: false,
streaming: false, streaming: false,
stream: undefined, stream: undefined,
dataImage: undefined dataImage: undefined
}), }),
methods: { methods: {
...mapMutations(['createToast']), ...mapMutations(['createToast']),
openStream() { openStream() {
if (!this.capturing) { if (!this.capturing) {
this.capturing = true; this.capturing = true;
this.streaming = false; this.streaming = false;
navigator.mediaDevices.getUserMedia({video: { facingMode: "environment" }, audio: false}).then(stream => { navigator.mediaDevices.getUserMedia({video: {facingMode: "environment"}, audio: false}).then(stream => {
this.stream = stream; this.stream = stream;
const { video } = this.$refs; const {video} = this.$refs;
video.srcObject = stream; video.srcObject = stream;
video.play(); video.play();
video.addEventListener('canplay', () => { video.addEventListener('canplay', () => {
this.streaming = true; this.streaming = true;
}, false); }, false);
}).catch(err => console.log(err)); // todo: toast error }).catch(err => console.log(err)); // todo: toast error
} }
},
captureVideoImage() {
const {video, canvas} = this.$refs;
const context = canvas.getContext('2d');
const {videoWidth, videoHeight} = video;
canvas.width = videoWidth;
canvas.height = videoHeight;
context.drawImage(video, 0, 0, videoWidth, videoHeight);
this.dataImage = canvas.toDataURL('image/png');
this.onCapture(this.dataImage);
this.closeStream();
},
closeStream() {
if (this.capturing) {
this.stream.getTracks().forEach(s => s.stop());
this.capturing = false;
this.streaming = false;
}
},
onFileChange({target}) {
const file = target.files[0];
var reader = new FileReader();
reader.readAsDataURL(file);
const self = this;
reader.onload = function () {
self.dataImage = reader.result;
self.onCapture(self.dataImage);
self.closeStream();
};
reader.onerror = function (error) {
this.createToast({
title: 'Error: Failed to parse image file',
message: error.toString(),
color: 'danger'
});
console.log('Error: ', error);
};
}
}, },
captureVideoImage() { mounted() {
const { video, canvas } = this.$refs; if (!this.model[this.field])
const context = canvas.getContext('2d'); this.openStream();
const { videoWidth, videoHeight } = video;
canvas.width = videoWidth;
canvas.height = videoHeight;
context.drawImage(video, 0, 0, videoWidth, videoHeight);
this.dataImage = canvas.toDataURL('image/png');
this.onCapture(this.dataImage);
this.closeStream();
}, },
closeStream() { beforeDestroy() {
if (this.capturing) { this.closeStream();
this.stream.getTracks().forEach(s => s.stop());
this.capturing = false;
this.streaming = false;
}
},
onFileChange({ target }) {
const file = target.files[0];
var reader = new FileReader();
reader.readAsDataURL(file);
const self = this;
reader.onload = function () {
self.dataImage = reader.result;
self.onCapture(self.dataImage);
self.closeStream();
};
reader.onerror = function (error) {
this.createToast({ title: 'Error: Failed to parse image file', message: error.toString(), color: 'danger' });
console.log('Error: ', error);
};
} }
},
mounted() {
if (!this.model[this.field])
this.openStream();
},
beforeDestroy() {
this.closeStream();
}
}; };
</script> </script>
<style> <style>
.img-preview{ .img-preview {
max-height: 30vh; max-height: 30vh;
} }
</style> </style>

View file

@ -13,17 +13,17 @@
import Addon from './Addon'; import Addon from './Addon';
export default { export default {
name: 'InputString', name: 'InputString',
components: { Addon }, components: {Addon},
props: [ 'label', 'model', 'field', 'validationFn' ], props: ['label', 'model', 'field', 'validationFn'],
computed: { computed: {
hasValidationFn({ validationFn }) { hasValidationFn({validationFn}) {
return validationFn != undefined; return validationFn != undefined;
}, },
isValid({ model, field, validationFn, hasValidationFn }) { isValid({model, field, validationFn, hasValidationFn}) {
if (!hasValidationFn) return true; if (!hasValidationFn) return true;
return validationFn(model[field]); return validationFn(model[field]);
}
} }
}
}; };
</script> </script>

View file

@ -1,6 +1,6 @@
import Vue from 'vue'; import Vue from 'vue';
import App from './App.vue'; import App from './App.vue';
import { sync } from 'vuex-router-sync'; import {sync} from 'vuex-router-sync';
import store from './store'; import store from './store';
import router from './router'; import router from './router';
@ -10,9 +10,28 @@ import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap/dist/js/bootstrap.min.js'; 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, faTh, faList, faWindowClose, faCamera, faStop, faPen, faCheck, faTimes, faSave } from '@fortawesome/free-solid-svg-icons'; import {
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; faPlus,
faCheckCircle,
faEdit,
faTrash,
faCat,
faSyncAlt,
faSort,
faSortUp,
faSortDown,
faTh,
faList,
faWindowClose,
faCamera,
faStop,
faPen,
faCheck,
faTimes,
faSave
} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome';
import vueDebounce from 'vue-debounce'; import vueDebounce from 'vue-debounce';
@ -23,10 +42,10 @@ Vue.component('font-awesome-icon', FontAwesomeIcon);
sync(store, router); sync(store, router);
new Vue({ new Vue({
el: '#app', el: '#app',
store, store,
router, router,
render: h => h(App), render: h => h(App),
}); });
Vue.use(vueDebounce); Vue.use(vueDebounce);

View file

@ -1,39 +1,39 @@
import * as R from 'ramda'; import * as R from 'ramda';
export default { export default {
props: ['columns', 'items', 'actions', 'keyName'], props: ['columns', 'items', 'actions', 'keyName'],
data: (self) => ({ data: (self) => ({
sortBy: self.keyName, sortBy: self.keyName,
ascend: true, ascend: true,
filters: R.fromPairs(self.columns.map(column => [column, ''])) filters: R.fromPairs(self.columns.map(column => [column, '']))
}), }),
computed: { computed: {
internalItems() { internalItems() {
const filtered = this.items.filter(item => this.columns const filtered = this.items.filter(item => this.columns
.map(column => { .map(column => {
const field = item[column] + ''; const field = item[column] + '';
const filter = this.filters[column]; const filter = this.filters[column];
return field.toLowerCase().includes(filter.toLowerCase()); return field.toLowerCase().includes(filter.toLowerCase());
}).reduce((acc, nxt) => acc && nxt, true) }).reduce((acc, nxt) => acc && nxt, true)
); );
const sortByOrd = R.sortBy(R.prop(this.sortBy)); const sortByOrd = R.sortBy(R.prop(this.sortBy));
const sorted = sortByOrd(filtered, [this.sortBy]); const sorted = sortByOrd(filtered, [this.sortBy]);
return this.ascend ? sorted : R.reverse(sorted); 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) { methods: {
if (column === this.sortBy) getSortIcon(column) {
this.ascend = !this.ascend; if (column !== this.sortBy) return 'sort';
this.sortBy = column; if (this.ascend) return 'sort-up';
}, return 'sort-down';
setFilter(column, filter) { },
this.filters[column] = filter; toggleSort(column) {
if (column === this.sortBy)
this.ascend = !this.ascend;
this.sortBy = column;
},
setFilter(column, filter) {
this.filters[column] = filter;
}
} }
}
}; };

View file

@ -8,24 +8,23 @@ import VueRouter from 'vue-router';
import Vue from 'vue'; import Vue from 'vue';
Vue.use(VueRouter); Vue.use(VueRouter);
const routes = [ const routes = [
{ path: '/', redirect: '/Camp23/items' }, {path: '/', redirect: '/Camp23/items'},
{ path: '/howto', name: 'howto', component: HowTo}, {path: '/howto', name: 'howto', component: HowTo},
{ path: '/admin/files', name: 'files', component: Files}, {path: '/admin/files', name: 'files', component: Files},
{ path: '/admin/events', name: 'events', component: Events}, {path: '/admin/events', name: 'events', component: Events},
{ path: '/:event/boxes', name: 'boxes', component: Boxes}, {path: '/:event/boxes', name: 'boxes', component: Boxes},
{ path: '/:event/items', name: 'items', component: Items}, {path: '/:event/items', name: 'items', component: Items},
{ path: '/:event/box/:uid', name: 'boxes', component: Boxes}, {path: '/:event/box/:uid', name: 'boxes', component: Boxes},
{ path: '/:event/item/:uid', name: 'items', component: Items}, {path: '/:event/item/:uid', name: 'items', component: Items},
{ path: '*', component: Error}, {path: '*', component: Error},
]; ];
const router = new VueRouter({ const router = new VueRouter({
mode: 'history', mode: 'history',
routes routes
}); });
export default router; export default router;

View file

@ -10,17 +10,17 @@ import * as utf8 from 'utf8';
Vue.use(Vuex); Vue.use(Vuex);
const axios = AxiosBootstrap.create({ const axios = AxiosBootstrap.create({
baseURL: config.service.url, baseURL: config.service.url,
auth: config.service.auth auth: config.service.auth
}); });
axios.interceptors.response.use(response => response, error => { axios.interceptors.response.use(response => response, error => {
console.log('error interceptor fired'); console.log('error interceptor fired');
console.error(error); // todo: toast error console.error(error); // todo: toast error
console.log(Object.entries(error)); console.log(Object.entries(error));
if (error.isAxiosError) { if (error.isAxiosError) {
const message = ` const message = `
<h3>A HTTP ${error.config.method} request failed.</h3> <h3>A HTTP ${error.config.method} request failed.</h3>
<p> <p>
url: ${error.config.url} url: ${error.config.url}
@ -30,120 +30,120 @@ axios.interceptors.response.use(response => response, error => {
response-body: ${error.response && error.response.body} response-body: ${error.response && error.response.body}
</p> </p>
`; `;
store.commit('createToast', {title: 'Error: HTTP', message, color: 'danger'}); store.commit('createToast', {title: 'Error: HTTP', message, color: 'danger'});
} else { } else {
store.commit('createToast', {title: 'Error: Unknown', message: error.toString(), color: 'danger'}); store.commit('createToast', {title: 'Error: Unknown', message: error.toString(), color: 'danger'});
} }
return Promise.reject(error); return Promise.reject(error);
}); });
const store = new Vuex.Store({ const store = new Vuex.Store({
state: { state: {
keyIncrement: 0, keyIncrement: 0,
events: [], events: [],
layout: 'cards', layout: 'cards',
loadedItems: [], loadedItems: [],
loadedBoxes: [], loadedBoxes: [],
toasts: [], toasts: [],
lastUsed: localStorage.getItem('lf_lastUsed') || {}, lastUsed: localStorage.getItem('lf_lastUsed') || {},
},
getters: {
getEventSlug: state => state.route && state.route.params.event? state.route.params.event : state.events.length ? state.events[0].slug : '36C3',
getActiveView: state => state.route.name || 'items',
getFilters: state => state.route.query,
getBoxes: state => state.loadedBoxes
},
mutations: {
updateLastUsed(state, diff) {
state.lastUsed = _.extend(state.lastUsed, diff);
localStorage.setItem('lf_lastUsed', state.lastUsed);
}, },
replaceEvents(state, events) { getters: {
state.events = events; getEventSlug: state => state.route && state.route.params.event ? state.route.params.event : state.events.length ? state.events[0].slug : '36C3',
getActiveView: state => state.route.name || 'items',
getFilters: state => state.route.query,
getBoxes: state => state.loadedBoxes
}, },
changeView(state, {view, slug}) { mutations: {
router.push({path: `/${slug}/${view}`}); updateLastUsed(state, diff) {
state.lastUsed = _.extend(state.lastUsed, diff);
localStorage.setItem('lf_lastUsed', state.lastUsed);
},
replaceEvents(state, events) {
state.events = events;
},
changeView(state, {view, slug}) {
router.push({path: `/${slug}/${view}`});
},
replaceLoadedItems(state, newItems) {
state.loadedItems = newItems;
},
setLayout(state, layout) {
state.layout = layout;
},
replaceBoxes(state, loadedBoxes) {
state.loadedBoxes = loadedBoxes;
},
updateItem(state, updatedItem) {
const item = state.loadedItems.filter(({uid}) => uid === updatedItem.uid)[0];
Object.assign(item, updatedItem);
},
removeItem(state, item) {
state.loadedItems = state.loadedItems.filter(it => it !== item);
},
appendItem(state, item) {
state.loadedItems.push(item);
},
createToast(state, {title, message, color}) {
state.toasts.push({title, message, color, key: state.keyIncrement});
state.keyIncrement += 1;
},
removeToast(state, key) {
state.toasts = state.toasts.filter(toast => toast.key !== key);
}
}, },
replaceLoadedItems(state, newItems) { actions: {
state.loadedItems = newItems; async loadEvents({commit}) {
}, const {data} = await axios.get('/1/events');
setLayout(state, layout) { commit('replaceEvents', data);
state.layout = layout; },
}, changeEvent({dispatch, getters}, eventName) {
replaceBoxes(state, loadedBoxes) { router.push({path: `/${eventName.slug}/${getters.getActiveView}`});
state.loadedBoxes = loadedBoxes; dispatch('loadEventItems');
}, },
updateItem(state, updatedItem) { changeView({getters}, link) {
const item = state.loadedItems.filter(({ uid }) => uid === updatedItem.uid)[0]; router.push({path: `/${getters.getEventSlug}/${link.path}`});
Object.assign(item, updatedItem); },
}, showBoxContent({getters}, box) {
removeItem(state, item) { router.push({path: `/${getters.getEventSlug}/items`, query: {box}});
state.loadedItems = state.loadedItems.filter(it => it !== item ); },
}, async loadEventItems({commit, getters}) {
appendItem(state, item) { const {data} = await axios.get(`/1/${getters.getEventSlug}/items`);
state.loadedItems.push(item); commit('replaceLoadedItems', data);
}, },
createToast(state, { title, message, color }) { async searchEventItems({commit, getters}, query) {
state.toasts.push({ title, message, color, key: state.keyIncrement }); const foo = utf8.encode(query);
state.keyIncrement += 1; const bar = base64.encode(foo);
},
removeToast(state, key) {
state.toasts = state.toasts.filter(toast => toast.key !== key);
}
},
actions: {
async loadEvents({ commit }) {
const { data } = await axios.get('/1/events');
commit('replaceEvents', data);
},
changeEvent({ dispatch, getters}, eventName) {
router.push({path: `/${eventName.slug}/${getters.getActiveView}`});
dispatch('loadEventItems');
},
changeView({ getters }, link) {
router.push({path: `/${getters.getEventSlug}/${link.path}`});
},
showBoxContent({ getters }, box) {
router.push({path: `/${getters.getEventSlug}/items`, query: {box}});
},
async loadEventItems({ commit, getters }) {
const { data } = await axios.get(`/1/${getters.getEventSlug}/items`);
commit('replaceLoadedItems', data);
},
async searchEventItems({ commit, getters }, query) {
const foo = utf8.encode(query);
const bar = base64.encode(foo);
const {data} = await axios.get(`/1/${getters.getEventSlug}/items/${bar}`); const {data} = await axios.get(`/1/${getters.getEventSlug}/items/${bar}`);
commit('replaceLoadedItems', data); commit('replaceLoadedItems', data);
}, },
async loadBoxes({ commit }) { async loadBoxes({commit}) {
const { data } = await axios.get('/1/boxes'); const {data} = await axios.get('/1/boxes');
commit('replaceBoxes', data); commit('replaceBoxes', data);
}, },
async updateItem({ commit, getters }, item) { async updateItem({commit, getters}, item) {
const { data } = await axios.put(`/1/${getters.getEventSlug}/item/${item.uid}`, item); const {data} = await axios.put(`/1/${getters.getEventSlug}/item/${item.uid}`, item);
commit('updateItem', data); commit('updateItem', data);
}, },
async markItemReturned({ commit, getters }, item) { async markItemReturned({commit, getters}, item) {
await axios.put(`/1/${getters.getEventSlug}/item/${item.uid}`, {returned: true}); await axios.put(`/1/${getters.getEventSlug}/item/${item.uid}`, {returned: true});
commit('removeItem', item); commit('removeItem', item);
}, },
async deleteItem({ commit, getters }, item) { async deleteItem({commit, getters}, item) {
await axios.delete(`/1/${getters.getEventSlug}/item/${item.uid}`, item); await axios.delete(`/1/${getters.getEventSlug}/item/${item.uid}`, item);
commit('removeItem',item); commit('removeItem', item);
}, },
async postItem({ commit, getters }, item) { async postItem({commit, getters}, item) {
commit('updateLastUsed',{box: item.box, cid: item.cid}); commit('updateLastUsed', {box: item.box, cid: item.cid});
const { data } = await axios.post(`/1/${getters.getEventSlug}/item`, item); const {data} = await axios.post(`/1/${getters.getEventSlug}/item`, item);
commit('appendItem', data); commit('appendItem', data);
}
} }
}
}); });
export default store; export default store;
store.dispatch('loadEvents').then(() =>{ store.dispatch('loadEvents').then(() => {
store.dispatch('loadEventItems'); store.dispatch('loadEventItems');
store.dispatch('loadBoxes'); store.dispatch('loadBoxes');
}); });

View file

@ -8,14 +8,14 @@
:keyName="'cid'" :keyName="'cid'"
v-slot="{ item }" v-slot="{ item }"
> >
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-secondary" @click.stop="showBoxContent(item.name)" > <button class="btn btn-secondary" @click.stop="showBoxContent(item.name)">
<!--font-awesome-icon icon="archive"/--> content <!--font-awesome-icon icon="archive"/--> content
</button> </button>
<button class="btn btn-danger" @click.stop="" title="delete"> <button class="btn btn-danger" @click.stop="" title="delete">
<font-awesome-icon icon="trash"/> <font-awesome-icon icon="trash"/>
</button> </button>
</div> </div>
</Table> </Table>
</div> </div>
</div> </div>
@ -27,10 +27,10 @@ import {mapActions, mapState} from 'vuex';
import Table from '@/components/Table'; import Table from '@/components/Table';
export default { export default {
name: 'Boxes', name: 'Boxes',
components: {Table}, components: {Table},
computed: mapState(['loadedBoxes', 'layout']), computed: mapState(['loadedBoxes', 'layout']),
methods: mapActions(['showBoxContent']), methods: mapActions(['showBoxContent']),
}; };
</script> </script>

View file

@ -4,7 +4,7 @@
<script> <script>
export default { export default {
name: 'Error', name: 'Error',
}; };
</script> </script>

View file

@ -1,25 +1,27 @@
<template> <template>
<div class="container-fluid px-xl-5 mt-3"> <div class="container-fluid px-xl-5 mt-3">
<div class="row"> <div class="row">
<div class="col-xl-8 offset-xl-2"> <div class="col-xl-8 offset-xl-2">
<Table <Table
:columns="['slug', 'name']" :columns="['slug', 'name']"
:items="events" :items="events"
:keyName="'slug'" :keyName="'slug'"
v-slot="{ item }" v-slot="{ item }"
> >
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-secondary" @click.stop="changeEvent(item)" > <button class="btn btn-secondary" @click.stop="changeEvent(item)">
<font-awesome-icon icon="archive"/> use <font-awesome-icon icon="archive"/>
</button> use
<button class="btn btn-danger" @click.stop="" > </button>
<font-awesome-icon icon="trash"/> delete <button class="btn btn-danger" @click.stop="">
</button> <font-awesome-icon icon="trash"/>
</div> delete
</Table> </button>
</div> </div>
</Table>
</div>
</div>
</div> </div>
</div>
</template> </template>
<script> <script>
@ -27,10 +29,10 @@ import {mapActions, mapState} from 'vuex';
import Table from '@/components/Table'; import Table from '@/components/Table';
export default { export default {
name: 'Events', name: 'Events',
components: {Table}, components: {Table},
computed: mapState(['events']), computed: mapState(['events']),
methods: mapActions(['changeEvent']), methods: mapActions(['changeEvent']),
}; };
</script> </script>

View file

@ -1,22 +1,23 @@
<template> <template>
<div class="container-fluid px-xl-5 mt-3"> <div class="container-fluid px-xl-5 mt-3">
<div class="row"> <div class="row">
<div class="col-xl-8 offset-xl-2"> <div class="col-xl-8 offset-xl-2">
<Table <Table
:columns="['hash', 'uid']" :columns="['hash', 'uid']"
:items="loadedItems" :items="loadedItems"
:keyName="'hash'" :keyName="'hash'"
v-slot="{ item }" v-slot="{ item }"
> >
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-danger" @click.stop="" > <button class="btn btn-danger" @click.stop="">
<font-awesome-icon icon="trash"/> delete <font-awesome-icon icon="trash"/>
</button> delete
</div> </button>
</Table> </div>
</div> </Table>
</div>
</div>
</div> </div>
</div>
</template> </template>
<script> <script>
@ -24,10 +25,10 @@ import {mapActions, mapState} from 'vuex';
import Table from '@/components/Table'; import Table from '@/components/Table';
export default { export default {
name: 'Files', name: 'Files',
components: {Table}, components: {Table},
computed: mapState(['loadedItems']), computed: mapState(['loadedItems']),
methods: mapActions([]), methods: mapActions([]),
}; };
</script> </script>

View file

@ -1,125 +1,135 @@
<template> <template>
<div class="container-fluid px-xl-5 mt-3" style="color: #f8f9fa !important;"> <div class="container-fluid px-xl-5 mt-3" style="color: #f8f9fa !important;">
<div class="row"> <div class="row">
<div class="col-xl-8 offset-xl-2"> <div class="col-xl-8 offset-xl-2">
<b>[en below]</b> <b>[en below]</b>
<h2>Lost&Found (Deutsche Version)</h2> <h2>Lost&Found (Deutsche Version)</h2>
<p>Herzlich Willkommen bei Lost&Found von $Veranstaltung!</p> <p>Herzlich Willkommen bei Lost&Found von $Veranstaltung!</p>
<p>Deine Aufgaben sind es verloren gegangene Gegenstände anzunehmen und zu registrieren, sowie <p>Deine Aufgaben sind es verloren gegangene Gegenstände anzunehmen und zu registrieren, sowie
Gegenstände ihren Besitzenden zurückzubringen.</p> Gegenstände ihren Besitzenden zurückzubringen.</p>
<p><b>Bitte den Inhalt des Lost+Founds nicht offen liegen lassen oder rumzeigen. Erst beschreiben <p><b>Bitte den Inhalt des Lost+Founds nicht offen liegen lassen oder rumzeigen. Erst beschreiben
lassen, dann zeigen.</b></p> lassen, dann zeigen.</b></p>
<h3>Found (Jemand bringt einen verloren gegangen Gegenstand vorbei)</h3> <h3>Found (Jemand bringt einen verloren gegangen Gegenstand vorbei)</h3>
<ul>
<li>Möglichst viele Informationen über die Umstände heraus
<ul>
<li>Wo wurde der Gegenstand gefunden?</li>
<li>Zu welcher Uhrzeit wurde der Gegenstand gefunden?</li>
</ul>
</li>
<li>Der Gegenstand wird in das c3lf-System eingetragen
<ul>
<li>Foto vom Gegenstand mit der Webcam machen</li>
<li>Beschreibung des Gegenstands eintragen (gerne wie Tags behandeln die durch Leerzeichen getrennt
sind)
</li>
<li>Nummer der Box eintragen in die der Gegenstand gelegt wird</li>
<li>Gegenstand in die Box legen</li>
</ul>
</li>
</ul>
<h3>Lost (Jemand versucht einen Gegenstand wiederzufinden)</h3>
<ul>
<li>Jemand möchte einen Gegenstand wiederfinden</li>
<li>Die Person soll möglichst genau beschreiben was sie sucht (insbesondere bei wertvolleren Dingen)</li>
<li>Du schaust im System nach ob der Gegenstand vorhanden ist</li>
<li>Wenn ein passender Gegenstand gefunden ist:
<ul>
<li>Gegenstand aus der Box holen und der Person aushändigen</li>
<li>Grünen Haken beim Gegenstand setzen</li>
</ul>
</li>
<li>Wenn kein passender Gegenstand existiert:
<ul>
<li>Sagen, dass es den Gegenstand nicht gibt</li>
<li>Darauf verweisen dass die Person entweder zu späterem Zeitpunkt wiederkommt, oder ein Ticket bei <a
href="mailto:camp23@c3lf.de" target="_blank" rel="noopener">camp23@c3lf.de</a> aufmacht. Außerdem ist es
gut möglich dass der Himmel und andere Villages eigene, kleinere Lost+Founds während des Events
aufgemacht haben. Rumfragen lohnt sich also.
<ul> <ul>
<li><b>Achtung: Tickets werden erst nach Ende des Events bearbeitet.</b></li> <li>Möglichst viele Informationen über die Umstände heraus
<ul>
<li>Wo wurde der Gegenstand gefunden?</li>
<li>Zu welcher Uhrzeit wurde der Gegenstand gefunden?</li>
</ul>
</li>
<li>Der Gegenstand wird in das c3lf-System eingetragen
<ul>
<li>Foto vom Gegenstand mit der Webcam machen</li>
<li>Beschreibung des Gegenstands eintragen (gerne wie Tags behandeln die durch Leerzeichen
getrennt
sind)
</li>
<li>Nummer der Box eintragen in die der Gegenstand gelegt wird</li>
<li>Gegenstand in die Box legen</li>
</ul>
</li>
</ul> </ul>
</li> <h3>Lost (Jemand versucht einen Gegenstand wiederzufinden)</h3>
</ul>
</li>
</ul>
<h3>Eskalation</h3>
<ul>
<li>Bei Lost&amp;Found bezogenen Problemen: 1033 (Lost&amp;Found Orga) anrufen</li>
<li>Bei Engel bezogenen Problemen: 1023 (Himmel) anrufen</li>
</ul>
<hr>
<h2>Lost&Found (English version)</h2>
<p>Welcome to Lost&Found of $Event!</p>
<p>Your tasks are to accept and register lost items, as well as to return items to their owners.</p>
<p><b>Please do not leave the contents of the Lost+Found lying around or show them around. Ask for a description of the item first, then show them (if the description seems correct).</b></p>
<h3>Found (Someone brings a lost item)</h3>
<ul>
<li>Get as much information as possible about the circumstances
<ul>
<li>Where was the item found?</li>
<li>At what time was the item found?</li>
</ul>
</li>
<li>The item is entered into the c3lf system
<ul>
<li>Take a photo of the item with the webcam</li>
<li>Enter a description of the item (preferably treat it like tags separated by spaces)</li>
<li>Enter the number of the box into which the item is to be placed</li>
<li>Place the item in the box</li>
</ul>
</li>
</ul>
<h3>Lost (Someone tries to find an item)</h3>
<ul>
<li>Someone wants to find an item</li>
<li>The person should describe as precisely as possible what they are looking for (especially for more
valuable things)
</li>
<li>You check the system to see if the item is available</li>
<li>If a suitable item is found:
<ul>
<li>Take the item out of the box and hand it over to the person</li>
<li>Set a green check mark for the item</li>
</ul>
</li>
<li>If no suitable item exists:
<ul>
<li>Say that the item does not exist</li>
<li>Point out that the person either comes back at a later time, or opens a ticket at <a
href="mailto:camp23@c3lf.de" target="_blank" rel="noopener">camp23@c3lf.de</a>. It is also possible that
the Himmel and other Villages have their own, smaller Lost+Founds during the event. So it's worth asking
around.
<ul> <ul>
<li><b>Attention: Tickets will not be processed until after the event.</b></li> <li>Jemand möchte einen Gegenstand wiederfinden</li>
<li>Die Person soll möglichst genau beschreiben was sie sucht (insbesondere bei wertvolleren
Dingen)
</li>
<li>Du schaust im System nach ob der Gegenstand vorhanden ist</li>
<li>Wenn ein passender Gegenstand gefunden ist:
<ul>
<li>Gegenstand aus der Box holen und der Person aushändigen</li>
<li>Grünen Haken beim Gegenstand setzen</li>
</ul>
</li>
<li>Wenn kein passender Gegenstand existiert:
<ul>
<li>Sagen, dass es den Gegenstand nicht gibt</li>
<li>Darauf verweisen dass die Person entweder zu späterem Zeitpunkt wiederkommt, oder ein
Ticket bei <a
href="mailto:camp23@c3lf.de" target="_blank" rel="noopener">camp23@c3lf.de</a>
aufmacht. Außerdem ist es
gut möglich dass der Himmel und andere Villages eigene, kleinere Lost+Founds während des
Events
aufgemacht haben. Rumfragen lohnt sich also.
<ul>
<li><b>Achtung: Tickets werden erst nach Ende des Events bearbeitet.</b></li>
</ul>
</li>
</ul>
</li>
</ul> </ul>
</li> <h3>Eskalation</h3>
</ul> <ul>
</li> <li>Bei Lost&amp;Found bezogenen Problemen: 1033 (Lost&amp;Found Orga) anrufen</li>
</ul> <li>Bei Engel bezogenen Problemen: 1023 (Himmel) anrufen</li>
<h3>Escalation</h3> </ul>
<ul> <hr>
<li>For Lost&amp;Found related problems: call 1033 (Lost&amp;Found Orga)</li> <h2>Lost&Found (English version)</h2>
<li>For angel related problems: call 1023 (Heaven)</li> <p>Welcome to Lost&Found of $Event!</p>
</ul> <p>Your tasks are to accept and register lost items, as well as to return items to their owners.</p>
</div> <p><b>Please do not leave the contents of the Lost+Found lying around or show them around. Ask for a
description of the item first, then show them (if the description seems correct).</b></p>
<h3>Found (Someone brings a lost item)</h3>
<ul>
<li>Get as much information as possible about the circumstances
<ul>
<li>Where was the item found?</li>
<li>At what time was the item found?</li>
</ul>
</li>
<li>The item is entered into the c3lf system
<ul>
<li>Take a photo of the item with the webcam</li>
<li>Enter a description of the item (preferably treat it like tags separated by spaces)</li>
<li>Enter the number of the box into which the item is to be placed</li>
<li>Place the item in the box</li>
</ul>
</li>
</ul>
<h3>Lost (Someone tries to find an item)</h3>
<ul>
<li>Someone wants to find an item</li>
<li>The person should describe as precisely as possible what they are looking for (especially for
more
valuable things)
</li>
<li>You check the system to see if the item is available</li>
<li>If a suitable item is found:
<ul>
<li>Take the item out of the box and hand it over to the person</li>
<li>Set a green check mark for the item</li>
</ul>
</li>
<li>If no suitable item exists:
<ul>
<li>Say that the item does not exist</li>
<li>Point out that the person either comes back at a later time, or opens a ticket at <a
href="mailto:camp23@c3lf.de" target="_blank" rel="noopener">camp23@c3lf.de</a>. It is
also possible that
the Himmel and other Villages have their own, smaller Lost+Founds during the event. So
it's worth asking
around.
<ul>
<li><b>Attention: Tickets will not be processed until after the event.</b></li>
</ul>
</li>
</ul>
</li>
</ul>
<h3>Escalation</h3>
<ul>
<li>For Lost&amp;Found related problems: call 1033 (Lost&amp;Found Orga)</li>
<li>For angel related problems: call 1023 (Heaven)</li>
</ul>
</div>
</div>
</div> </div>
</div>
</template> </template>
<script> <script>
export default { export default {
name: 'HowTo', name: 'HowTo',
}; };
</script> </script>

View file

@ -22,17 +22,17 @@
v-slot="{ item }" v-slot="{ item }"
@itemActivated="openLightboxModalWith($event)" @itemActivated="openLightboxModalWith($event)"
> >
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-success" @click.stop="markItemReturned(item)" title="returned"> <button class="btn btn-success" @click.stop="markItemReturned(item)" title="returned">
<font-awesome-icon icon="check"/> <font-awesome-icon icon="check"/>
</button> </button>
<button class="btn btn-secondary" @click.stop="openEditingModalWith(item)" title="edit"> <button class="btn btn-secondary" @click.stop="openEditingModalWith(item)" title="edit">
<font-awesome-icon icon="edit"/> <font-awesome-icon icon="edit"/>
</button> </button>
<button class="btn btn-danger" @click.stop="deleteItem(item)" title="delete"> <button class="btn btn-danger" @click.stop="deleteItem(item)" title="delete">
<font-awesome-icon icon="trash"/> <font-awesome-icon icon="trash"/>
</button> </button>
</div> </div>
</Table> </Table>
</div> </div>
</div> </div>
@ -53,14 +53,14 @@
<h6 class="card-subtitle text-secondary">uid: {{ item.uid }} box: {{ item.box }}</h6> <h6 class="card-subtitle text-secondary">uid: {{ item.uid }} box: {{ item.box }}</h6>
<div class="row mx-auto mt-2"> <div class="row mx-auto mt-2">
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-outline-success" @click.stop="markItemReturned(item)" title="returned"> <button class="btn btn-outline-success" @click.stop="markItemReturned(item)" title="returned">
<font-awesome-icon icon="check"/> <font-awesome-icon icon="check"/>
</button> </button>
<button class="btn btn-outline-secondary" @click.stop="openEditingModalWith(item)" title="edit"> <button class="btn btn-outline-secondary" @click.stop="openEditingModalWith(item)" title="edit">
<font-awesome-icon icon="edit"/> <font-awesome-icon icon="edit"/>
</button> </button>
<button class="btn btn-outline-danger" @click.stop="deleteItem(item)" title="delete"> <button class="btn btn-outline-danger" @click.stop="deleteItem(item)" title="delete">
<font-awesome-icon icon="trash"/> <font-awesome-icon icon="trash"/>
</button> </button>
</div> </div>
</div> </div>
@ -79,33 +79,33 @@ import config from '../config';
import Lightbox from '../components/Lightbox'; import Lightbox from '../components/Lightbox';
export default { export default {
name: 'Items', name: 'Items',
data: () => ({ data: () => ({
lightboxItem: null, lightboxItem: null,
editingItem: null, editingItem: null,
baseUrl: config.service.url, baseUrl: config.service.url,
}), }),
components: {Lightbox, Table, Cards, Modal, EditItem }, components: {Lightbox, Table, Cards, Modal, EditItem},
computed: mapState(['loadedItems', 'layout']), computed: mapState(['loadedItems', 'layout']),
methods: { methods: {
...mapActions(['deleteItem','markItemReturned']), ...mapActions(['deleteItem', 'markItemReturned']),
openLightboxModalWith(item) { // Opens the editing modal with a copy of the selected item. openLightboxModalWith(item) { // Opens the editing modal with a copy of the selected item.
this.lightboxItem = { ...item }; this.lightboxItem = {...item};
}, },
closeLightboxModal() { // Closes the editing modal and discards the edited copy of the item. closeLightboxModal() { // Closes the editing modal and discards the edited copy of the item.
this.lightboxItem = null; this.lightboxItem = null;
}, },
openEditingModalWith(item) { openEditingModalWith(item) {
this.editingItem = item; this.editingItem = item;
}, },
closeEditingModal() { closeEditingModal() {
this.editingItem = null; this.editingItem = null;
}, },
saveEditingItem() { // Saves the edited copy of the item. saveEditingItem() { // Saves the edited copy of the item.
this.$store.dispatch('updateItem', this.editingItem); this.$store.dispatch('updateItem', this.editingItem);
this.closeEditingModal(); this.closeEditingModal();
}
} }
}
}; };
</script> </script>