Restructuring for new way of installing libreevent

This commit is contained in:
2024-08-26 11:16:28 +02:00
parent 4d0b8eb1cb
commit 688b0616cc
223 changed files with 11 additions and 58 deletions

View File

@@ -1,4 +0,0 @@
> 1%
last 2 versions
not dead
not ie 11

View File

@@ -1,73 +0,0 @@
/*
* libreevent - .eslintrc.js
*
* Created by Janis Hutz 02/26/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
module.exports = {
'env': {
'browser': true,
'commonjs': true,
'es2021': true,
'node': true
},
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended'
],
'overrides': [
],
'parserOptions': {
'ecmaVersion': 'latest'
},
'rules': {
'indent': [
'error',
4
],
'linebreak-style': [
'error',
'unix'
],
'quotes': [
'error',
'single'
],
'semi': [
'error',
'always'
],
'spaced-comment': [
'error',
'always'
],
'arrow-spacing': [
'error',
{ 'before': true, 'after': true }
],
'func-call-spacing': [
'error',
'never'
],
'keyword-spacing': [
'error',
{ 'before': true, 'after': true }
],
'key-spacing': [
'error',
{ 'mode': 'strict' }
],
'space-before-blocks': [
'error',
'always'
],
'space-in-parens': [
'error',
'always'
],
'no-var': 'error'
}
};

View File

@@ -1,23 +0,0 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,19 +0,0 @@
# libreevent
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<link rel="icon" href="/favicon.ico">
<title>
libreevent
</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200">
<script defer src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<!-- <script type="text/javascript" src="https://cdn.jsdelivr.net/gh/isocroft/browsengine@0.2.1/dist/browsengine.min.js"></script> -->
<link rel="stylesheet" href="/coloris.min.css">
<script defer src="/coloris.min.js"></script>
</head>
<body>
<noscript>
<strong>We're sorry but this app doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -1,19 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +0,0 @@
{
"name": "libreevent",
"version": "1.0.7",
"private": false,
"scripts": {
"dev": "vite --host",
"preview": "vite preview --host",
"build": "vite build",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
},
"dependencies": {
"@pdfme/generator": "^1.2.3",
"@pdfme/ui": "^3.2.1",
"pinia": "^2.0.34",
"vue": "^3.2.13",
"vue-picture-input": "^3.0.1",
"vue-router": "^4.0.3",
"vue3-draggable-resizable": "^1.6.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.10.2",
"vite": "^2.9.18",
"eslint": "^8.39.0",
"eslint-plugin-vue": "^9.17.0"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,24 +0,0 @@
<html><head>
<style type="text/css">
#a {
margin:0 10px 10px;
}
#b {
width:100%;
}
</style>
<title>IE Crasher</title>
</head>
<body>
<table><tr><td>
<div id="a">
<form id="b">
<input type="text" name="test"/>
</div>
</td><td width="1"></td></tr></table>
</body></html>

View File

@@ -1,215 +0,0 @@
<!--
* libreevent - App.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<nav>
<!-- <p class="beta">LIBREEVENT BETA! Although this is a very late beta, bugs may still appear</p> -->
<a href="/">Home</a> |
<router-link to="/tickets">Tickets</router-link> |
<router-link to="/cart">Cart</router-link> |
<router-link to="/account">Account</router-link> |
<button @click="changeTheme();" v-html="theme" id="themeSelector"></button>
</nav>
<router-view v-slot="{ Component, route }">
<transition :name="route.meta.transition || 'fade'" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</template>
<style>
.beta {
color: red;
padding: 0;
margin: 0;
font-weight: bold;
}
body {
background-color: var( --background-color );
}
:root, :root.light {
--primary-color: #2c3e50;
--accent-background: rgb(30, 30, 82);
--secondary-color: white;
--background-color: white;
--popup-color: rgb(224, 224, 224);
--accent-color: #42b983;
--hover-color: rgb(165, 165, 165);
--accent-background-hover: #648cab;
--overlay-color: rgba(0, 0, 0, 0.7);
--inactive-color: rgb(100, 100, 100);
--hint-color: rgb(174, 210, 221);
--PI: 3.14159265358979;
}
:root.dark {
--primary-color: white;
--accent-background: rgb(20, 20, 116);
--secondary-color: white;
--background-color: rgb(32, 32, 32);
--popup-color: rgb(58, 58, 58);
--accent-color: #42b983;
--hover-color: rgb(83, 83, 83);
--accent-background-hover: #648cab;
--overlay-color: rgba(104, 104, 104, 0.575);
--inactive-color: rgb(190, 190, 190);
--hint-color: rgb(88, 91, 110);
}
@media ( prefers-color-scheme: dark ) {
:root {
--primary-color: white;
--accent-background: rgb(20, 20, 116);
--secondary-color: white;
--background-color: rgb(32, 32, 32);
--popup-color: rgb(58, 58, 58);
--accent-color: #42b983;
--hover-color: rgb(83, 83, 83);
--accent-background-hover: rgb(124, 140, 236);
--overlay-color: rgba(104, 104, 104, 0.575);
--inactive-color: rgb(190, 190, 190);
--hint-color: rgb(88, 91, 110);
}
}
::selection {
background-color: #7c8cec;
color: var( --secondary-color );
}
#themeSelector {
background-color: rgba( 0, 0, 0, 0 );
color: var( --primary-color );
font-size: 130%;
padding: 0;
margin: 0;
border: none;
cursor: pointer;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
#app {
transition: 0.5s;
background-color: var( --background-color );
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: var( --primary-color );
height: 100%;
display: flex;
flex-direction: column;
}
nav {
padding: 30px;
}
nav a {
font-weight: bold;
color: var( --primary-color );
}
nav a.router-link-exact-active {
color: #2080ca;
}
.scale-enter-active,
.scale-leave-active {
transition: all 0.5s ease;
}
.scale-enter-from,
.scale-leave-to {
opacity: 0;
transform: scale(0.9);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.4s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 48
}
.clr-open {
border: black solid 1px !important;
}
</style>
<script>
export default {
name: 'app',
data () {
return {
theme: '',
};
},
methods: {
changeTheme () {
if ( this.theme === '&#9788;' ) {
document.documentElement.classList.remove( 'dark' );
document.documentElement.classList.add( 'light' );
localStorage.setItem( 'theme', '&#9789;' );
this.theme = '&#9789;';
} else if ( this.theme === '&#9789;' ) {
document.documentElement.classList.remove( 'light' );
document.documentElement.classList.add( 'dark' );
localStorage.setItem( 'theme', '&#9788;' );
this.theme = '&#9788;';
}
}
},
created () {
this.theme = localStorage.getItem( 'theme' ) ?? '';
if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches || this.theme === '&#9788;' ) {
document.documentElement.classList.add( 'dark' );
this.theme = '&#9788;';
} else {
document.documentElement.classList.add( 'light' );
this.theme = '&#9789;';
}
console.log( `
_ _ _ _
| (_) | | |
| |_| |__ _ __ ___ _____ _____ _ __ | |_
| | | '_ \\| '__/ _ \\/ _ \\ \\ / / _ \\ '_ \\| __|
| | | |_) | | | __/ __/\\ V / __/ | | | |_
|_|_|_.__/|_| \\___|\\___| \\_/ \\___|_| |_|\\__|
You opened the developer tools. Know some coding? Want to help make this software even better?
Then come and join the development team of libreevent, the free and open source event management
solution. Your help is greatly appreciated by the team as well as all its users!
=> https://github.com/simplePCBuilding/libreevent
=> https://libreevent.janishutz.com/
` );
}
};
</script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 KiB

View File

@@ -1,354 +0,0 @@
<template>
<div class="seatingWrapper">
<div class="noseatplan">
<h2>Available tickets</h2>
<button @click="cartHandling()" class="button">Add selected tickets to cart</button>
<div class="wrapper">
<div v-for="ticket in event.categories" style="margin-bottom: 1.5%;">
<h4 style="margin: 0.5%;">{{ event[ 'categories' ][ ticket.id ][ 'name' ] }}</h4>
<table>
<tr v-for="ticketOption in event[ 'ageGroups' ]">
<td>
{{ ticketOption.name }} <div style="display: inline" v-if="ticketOption.age">({{ ticketOption.age }} years)</div>
</td>
<td>
{{ event.currency }} {{ event[ 'categories' ][ ticket.id ][ 'price' ][ ticketOption.id ] }}
</td>
<td>
<span class="material-symbols-outlined controls" @click="ticketHandling( 'ticket' + ticket.id, ticketOption.id, 'select' )">add</span>
{{ selectedTickets[ 'ticket' + ticket.id + '_' + ticketOption.id ] ?? 0 }}
<span class="material-symbols-outlined controls" @click="ticketHandling( 'ticket' + ticket.id, ticketOption.id, 'deselect' )">remove</span>
</td>
</tr>
</table>
</div>
</div>
</div>
<sideCartView :cart="cart" :name="event.name" ref="cart" type="false"></sideCartView>
<popups ref="popups" size="normal"></popups>
</div>
</template>
<script>
import sideCartView from '@/components/sideCartView.vue';
import popups from '@/components/notifications/popups.vue';
export default {
name: 'noseatplan',
props: {
ticketID: String
},
components: {
sideCartView,
popups,
},
data () {
return {
tickets: { 'ticket1': 20, 'ticket2': 20 },
event: { 'name': 'TestEvent2', 'location': 'TestLocation2', 'eventID': 'test2', 'date': '2023-07-15', 'currency': 'CHF', 'categories': { '1': { 'price': { '1': 25, '2': 35 }, 'bg': 'black', 'fg': 'white', 'name': 'Category 1', 'id': 1 }, '2': { 'price': { '1': 15, '2': 20 }, 'bg': 'green', 'fg': 'white', 'name': 'Category 2', 'id': 2 } }, 'ageGroups': { '1': { 'id': 1, 'name': 'Child', 'age': '0 - 15.99' }, '2': { 'id': 2, 'name': 'Adult' } }, 'maxTickets': 2 },
cart: {},
selectedTickets: {},
maxTickets: 10,
};
},
methods: {
ticketHandling( id, option, operation ) {
if ( operation === 'select' ) {
let totalTicketsPerID = {};
// sum up total of tickets per category (based on a sliced ID of the ticket selected,
// as ticketID is based on category and ageGroup)
for ( let ticket in this.selectedTickets ) {
if ( !totalTicketsPerID[ ticket.slice( 0, ticket.indexOf( '_' ) ) ] ) {
totalTicketsPerID[ ticket.slice( 0, ticket.indexOf( '_' ) ) ] = 0;
}
totalTicketsPerID[ ticket.slice( 0, ticket.indexOf( '_' ) ) ] += this.selectedTickets[ ticket ];
}
if ( !totalTicketsPerID[ id ] ) {
totalTicketsPerID[ id ] = 0;
}
totalTicketsPerID[ id ] += 1;
let totalTickets = 0;
for ( let category in totalTicketsPerID ) {
totalTickets += totalTicketsPerID[ category ];
}
if ( totalTickets <= this.maxTickets ) {
if ( totalTicketsPerID[ id ] <= this.tickets[ id ] ) {
if ( !this.selectedTickets[ id + '_' + option ] ) {
this.selectedTickets[ id + '_' + option ] = 0;
}
this.selectedTickets[ id + '_' + option ] += 1;
}
} else {
this.$refs.popups.openPopup( 'We are sorry, but you have already selected the maximum amount of tickets you can buy at once.', {}, 'string' );
}
} else {
if ( !this.selectedTickets[ id + '_' + option ] || this.selectedTickets[ id + '_' + option ] === 0 ) {
this.selectedTickets[ id + '_' + option ] = 0;
} else {
this.selectedTickets[ id + '_' + option ] -= 1;
}
}
},
seatChecks () {
let self = this;
let allSeatsAvailable = true;
fetch( localStorage.getItem( 'url' ) + '/getAPI/getReservedSeats?event=' + this.event.eventID ).then( res => {
if ( res.status === 200 ) {
let unavailableSeats = {};
res.json().then( data => {
console.log( data );
for ( let seat in data.reserved ) {
if ( data.reserved[ seat ] ) {
if ( !unavailableSeats[ data.reserved[ seat ].component ] ) {
unavailableSeats[ data.reserved[ seat ].component ] = {};
}
unavailableSeats[ data.reserved[ seat ].component ][ data.reserved[ seat ].id ] = 'nav';
}
}
for ( let seat in data.user ) {
if ( data.user[ seat ] ) {
if ( !unavailableSeats[ data.user[ seat ].component ] ) {
unavailableSeats[ data.user[ seat ].component ] = {};
}
unavailableSeats[ data.user[ seat ].component ][ data.user[ seat ].id ] = 'sel';
}
}
let tickets = {};
if ( this.cart[ this.event.eventID ] ) {
tickets = this.cart[ this.event.eventID ][ 'tickets' ];
}
if ( data.user ) {
for ( let element in tickets ) {
if ( !data.user[ element ] ) {
allSeatsAvailable = false;
if ( Object.keys( this.cart[ this.event.eventID ][ 'tickets' ] ).length > 1 ) {
delete this.cart[ this.event.eventID ][ 'tickets' ][ element ];
} else {
delete this.cart[ this.event.eventID ];
}
}
}
} else {
delete this.cart[ this.event.eventID ];
allSeatsAvailable = false;
}
// Create selectedTickets object
if ( this.cart[ this.event.eventID ] ) {
for ( let ticket in this.cart[ this.event.eventID ][ 'tickets' ] ) {
if ( this.cart[ this.event.eventID ][ 'tickets' ][ ticket ][ 'count' ] ) {
this.selectedTickets[ this.cart[ this.event.eventID ][ 'tickets' ][ ticket ][ 'id' ] ] = this.cart[ this.event.eventID ][ 'tickets' ][ ticket ][ 'count' ];
} else {
this.selectedTickets[ this.cart[ this.event.eventID ][ 'tickets' ][ ticket ][ 'id' ] ] = 0;
}
}
}
this.unavailableSeats = unavailableSeats;
if ( !allSeatsAvailable ) {
setTimeout( () => {
self.$refs.popups.openPopup( 'We are sorry to tell you that since the last time the seat plan was refreshed, one or more of the seats you have selected has/have been taken.', {}, 'string' );
}, 500 );
localStorage.setItem( 'cart', JSON.stringify( this.cart ) );
}
} );
} else {
console.error( 'unable to load' );
}
} );
},
cartHandling () {
let tickets = Object.keys( this.selectedTickets );
let ticket, ready = true;
let postInterval = setInterval( () => {
if ( ready ) {
ready = false;
if ( tickets.length > 0 ) {
ticket = tickets.pop();
let category = '';
const ticketSlice = ticket.slice( 0, ticket.indexOf( '_' ) );
for ( let letter in ticketSlice ) {
if ( !isNaN( ticketSlice[ letter ] ) ) {
category += parseInt( ticketSlice[ letter ] );
}
}
const options = {
method: 'post',
body: JSON.stringify( {
'id': ticket,
'component': 1,
'ticketOption': ticket.substring( ticket.indexOf( '_' ) + 1 ),
'eventID': this.event.eventID,
'count': this.selectedTickets[ ticket ],
'category': category,
'name': this.event.categories[ category ].name + ' (' + this.event.ageGroups[ ticket.substring( ticket.indexOf( '_' ) + 1 ) ].name + ')',
} ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/API/reserveTicket', options ).then( res => {
ready = true;
if ( !this.cart[ this.event.eventID ] ) {
this.cart[ this.event.eventID ] = { 'displayName': this.event.name, 'tickets': {}, 'eventID': this.event.eventID };
}
if ( res.status === 200 ) {
// add item to cart
if ( this.selectedTickets[ ticket ] < 1 ) {
if ( Object.keys( this.cart[ this.event.eventID ][ 'tickets' ] ).length <= 1 ) {
try {
delete this.cart[ this.event.eventID ];
} catch {
console.log( 'element nonexistent' );
}
} else {
delete this.cart[ this.event.eventID ][ 'tickets' ][ ticket ];
}
} else {
this.cart[ this.event.eventID ][ 'tickets' ][ ticket ] = {
'displayName': this.event.categories[ ticket.slice( ticket.indexOf( '_' ) - 1, ticket.indexOf( '_' ) ) ].name + ' (' + this.event.ageGroups[ ticket.substring( ticket.indexOf( '_' ) + 1 ) ].name + ')',
'price': this.event.categories[ ticket.slice( ticket.indexOf( '_' ) - 1, ticket.indexOf( '_' ) ) ].price[ ticket.substring( ticket.indexOf( '_' ) + 1 ) ],
'id': ticket,
'count': this.selectedTickets[ ticket ],
'comp': 1,
};
}
} else if ( res.status === 409 ) {
res.json().then( dat => {
if ( !this.cart[ this.event.eventID ] ) {
this.cart[ this.event.eventID ] = { 'displayName': this.event.name, 'tickets': {}, 'eventID': this.event.eventID };
}
if ( dat.count < 1 ) {
if ( Object.keys( this.cart[ this.event.eventID ][ 'tickets' ] ).length <= 1 ) {
try {
delete this.cart[ this.event.eventID ];
} catch {
console.log( 'element nonexistent' );
}
} else {
delete this.cart[ this.event.eventID ][ 'tickets' ][ ticket ];
}
} else {
this.cart[ this.event.eventID ][ 'tickets' ][ ticket ] = {
'displayName': this.event.categories[ ticket.slice( ticket.indexOf( '_' ) - 1, ticket.indexOf( '_' ) ) ].name + ' (' + this.event.ageGroups[ ticket.substring( ticket.indexOf( '_' ) + 1 ) ].name + ')',
'price': this.event.categories[ ticket.slice( ticket.indexOf( '_' ) - 1, ticket.indexOf( '_' ) ) ].price[ ticket.substring( ticket.indexOf( '_' ) + 1 ) ],
'id': ticket,
'count': dat.count,
'comp': 1,
};
}
this.selectedTickets[ ticket ] = dat.count;
} );
setTimeout( () => {
this.$refs.popups.openPopup( 'Unfortunately, you have selected more tickets than were still available. The maximum amount of tickets that are available have been selected for you automatically. We are sorry for the inconvenience.', {}, 'string' );
}, 300 );
} else if ( res.status === 418 ) {
res.json().then( dat => {
this.selectedTickets[ ticket ] = dat.count;
} );
setTimeout( () => {
this.$refs.popups.openPopup( 'We are sorry, but you have already selected the maximum amount of tickets you can buy at once.', {}, 'string' );
}, 300 );
}
if ( Object.keys( this.cart[ this.event.eventID ][ 'tickets' ] ).length < 1 ) {
delete this.cart[ this.event.eventID ];
}
this.$refs.cart.calculateTotal();
localStorage.setItem( 'cart', JSON.stringify( this.cart ) );
} );
} else {
clearInterval( postInterval );
}
}
} );
},
loadTickets () {
fetch( '/getAPI/getEvent?event=' + sessionStorage.getItem( 'selectedTicket' ) ).then( res => {
if ( res.status === 200 ) {
res.json().then( json => {
this.event = json ?? {};
this.seatChecks();
} );
}
} );
}
},
created () {
window.addEventListener( 'visibilitychange', () => {
this.cart = localStorage.getItem( 'cart' ) ? JSON.parse( localStorage.getItem( 'cart' ) ): {};
}, 1 );
this.cart = localStorage.getItem( 'cart' ) ? JSON.parse( localStorage.getItem( 'cart' ) ): {};
this.loadTickets();
}
};
</script>
<style scoped>
.seatingWrapper {
display: grid;
grid-template-areas:
'main main main sidebar'
'main main main sidebar'
'main main main sidebar'
'main main main sidebar'
'main main main sidebar'
'main main main sidebar'
'main main main sidebar'
'main main main sidebar'
'main main main sidebar';
height: 100%;
}
.button {
transition: all 1s;
padding: 1%;
border-radius: 50px;
border: none;
background-color: var( --accent-background-hover );
cursor: pointer;
color: var( --secondary-color );
margin-bottom: 3%;
text-decoration: none;
}
.button:hover {
background-color: var( --accent-background );
border-radius: 10px;
}
.noseatplan {
grid-area: main;
display: flex;
flex-direction: column;
align-items: center;
justify-content: justify;
overflow: scroll;
}
.wrapper {
width: 30%;
display: flex;
flex-direction: column;
align-items: justify;
justify-content: justify;
text-align: justify;
}
.controls {
user-select: none;
cursor: pointer;
font-size: 100%;
font-weight: bold;
border: solid var( --primary-color ) 1px;
border-radius: 100%;
}
</style>

View File

@@ -1,281 +0,0 @@
<!-- eslint-disable no-undef -->
<template>
<div id="notifications" @click="handleNotifications();">
<div class="message-box" :class="[ location, size ]">
<div class="message-container" :class="messageType">
<span class="material-symbols-outlined types hide" v-if="messageType == 'hide'">question_mark</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'ok'" style="background-color: green;">done</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'error'" style="background-color: red;">close</span>
<span class="material-symbols-outlined types progress-spinner" v-else-if="messageType == 'progress'" style="background-color: blue;">progress_activity</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'info'" style="background-color: lightblue;">info</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'warning'" style="background-color: orangered;">warning</span>
<p class="message">{{ message }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'notifications',
props: {
location: {
type: String,
'default': 'topleft',
},
size: {
type: String,
'default': 'default',
}
// Size options: small, default (default option), big, bigger, huge
},
data () {
return {
notifications: {},
queue: [],
message: '',
messageType: 'hide',
notificationDisplayTime: 0,
notificationPriority: 'normal',
currentlyDisplayedNotificationID: 0,
currentID: { 'critical': 0, 'medium': 1000, 'low': 100000 },
displayTimeCurrentNotification: 0,
notificationScheduler: null,
};
},
methods: {
createNotification( message, showDuration, messageType, priority ) {
/*
Takes a notification options array that contains: message, showDuration (in seconds), messageType (ok, error, progress, info) and priority (low, normal, critical).
Returns a notification ID which can be used to cancel the notification. The component will throttle notifications and display
one at a time and prioritize messages with higher priority. Use vue refs to access these methods.
*/
let id = 0;
if ( priority === 'critical' ) {
this.currentID[ 'critical' ] += 1;
id = this.currentID[ 'critical' ];
} else if ( priority === 'normal' ) {
this.currentID[ 'medium' ] += 1;
id = this.currentID[ 'medium' ];
} else if ( priority === 'low' ) {
this.currentID[ 'low' ] += 1;
id = this.currentID[ 'low' ];
}
this.notifications[ id ] = { 'message': message, 'showDuration': showDuration, 'messageType': messageType, 'priority': priority, 'id': id };
this.queue.push( id );
console.log( 'scheduled notification: ' + id + ' (' + message + ')' );
if ( this.displayTimeCurrentNotification >= this.notificationDisplayTime ) {
this.handleNotifications();
}
return id;
},
cancelNotification ( id ) {
/*
This method deletes a notification and, in case the notification is being displayed, hides it.
*/
try {
delete this.notifications[ id ];
} catch ( error ) {
console.log( 'notification to be deleted is nonexistent or currently being displayed' );
}
try {
this.queue.splice( this.queue.indexOf( id ), 1 );
} catch {
console.debug( 'queue empty' );
}
if ( this.currentlyDisplayedNotificationID == id ) {
this.handleNotifications();
}
},
handleNotifications () {
/*
This methods should NOT be called in any other component than this one!
*/
this.displayTimeCurrentNotification = 0;
this.notificationDisplayTime = 0;
this.message = '';
this.queue.sort();
if ( this.queue.length > 0 ) {
this.message = this.notifications[ this.queue[ 0 ] ][ 'message' ];
this.messageType = this.notifications[ this.queue[ 0 ] ][ 'messageType' ];
this.priority = this.notifications[ this.queue[ 0 ] ][ 'priority' ];
this.currentlyDisplayedNotificationID = this.notifications[ this.queue[ 0 ] ][ 'id' ];
this.notificationDisplayTime = this.notifications[ this.queue[ 0 ] ][ 'showDuration' ];
delete this.notifications[ this.queue[ 0 ] ];
this.queue.reverse();
this.queue.pop();
$( '.message-box' ).css( 'z-index', 20 );
} else {
this.messageType = 'hide';
$( '.message-box' ).css( 'z-index', -1 );
}
}
},
created () {
this.notificationScheduler = setInterval( () => {
if ( this.displayTimeCurrentNotification >= this.notificationDisplayTime ) {
this.handleNotifications();
} else {
this.displayTimeCurrentNotification += 0.5;
}
}, 500 );
},
unmounted ( ) {
clearInterval( this.notificationScheduler );
}
};
</script>
<style scoped>
.message-box {
position: fixed;
z-index: -1;
color: white;
transition: all 0.5s;
width: 95vw;
right: 2.5vw;
top: 1vh;
height: 10vh;
}
.message-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
opacity: 1;
transition: all 0.5s;
cursor: default;
}
.types {
color: white;
border-radius: 100%;
margin-right: auto;
margin-left: 5%;
padding: 1.5%;
font-size: 200%;
}
.message {
margin-right: 5%;
text-align: end;
}
.ok {
background-color: rgb(1, 71, 1);
}
.error {
background-color: rgb(114, 1, 1);
}
.info {
background-color: rgb(44, 112, 151);
}
.warning {
background-color: orange;
}
.hide {
opacity: 0;
}
.progress {
z-index: 20;
background-color: rgb(0, 0, 99);
}
.progress-spinner {
animation: spin 2s infinite linear;
}
@keyframes spin {
from {
transform: rotate( 0deg );
}
to {
transform: rotate( 720deg );
}
}
@media only screen and (min-width: 750px) {
.default {
height: 10vh;
width: 32vw;
}
.small {
height: 7vh;
width: 27vw;
}
.big {
height: 12vh;
width: 38vw;
}
.bigger {
height: 15vh;
width: 43vw;
}
.huge {
height: 20vh;
width: 50vw;
}
.topleft {
top: 3vh;
left: 0.5vw;
}
.topright {
top: 3vh;
right: 0.5vw;
}
.bottomright {
bottom: 3vh;
right: 0.5vw;
}
.bottomleft {
top: 3vh;
right: 0.5vw;
}
}
@media only screen and (min-width: 1500px) {
.default {
height: 10vh;
width: 15vw;
}
.small {
height: 7vh;
width: 11vw;
}
.big {
height: 12vh;
width: 17vw;
}
.bigger {
height: 15vh;
width: 20vw;
}
.huge {
height: 20vh;
width: 25vw;
}
}
</style>

View File

@@ -1,315 +0,0 @@
<!-- eslint-disable no-undef -->
<template>
<div id="popup-backdrop">
<div class="popup-container">
<div class="popup" :class="size">
<div class="close-wrapper"><span class="material-symbols-outlined close-button" @click="closePopup( 'cancel' );" title="Close this popup">close</span></div>
<div class="message-container">
<div v-if="contentType === 'string'" class="options">
<h3>{{ data.message }}</h3>
<div class="button-wrapper">
<button @click="closePopup( 'ok' )" title="Close popup">Ok</button>
</div>
</div>
<div v-else-if="contentType === 'html'" v-html="data.message" class="options"></div>
<div v-else-if="contentType === 'text'" class="options">
<h3>{{ data.message }}</h3>
<input type="text" v-model="data.selected">
<p>{{ info }}</p>
<div class="button-wrapper">
<button @click="closePopup( 'ok' )" title="Save changes">Save</button>
<button @click="closePopup( 'cancel' )" title="Cancel changes">Cancel</button>
</div>
</div>
<div v-else-if="contentType === 'number'" class="options">
<h3>{{ data.message }}</h3>
<input type="number" v-model="data.selected">
<div class="button-wrapper">
<button @click="closePopup( 'ok' )" title="Save changes">Save</button>
<button @click="closePopup( 'cancel' )" title="Cancel changes">Cancel</button>
</div>
</div>
<div v-else-if="contentType === 'settings'" class="options">
<h3>{{ data.message }}</h3>
<settings v-model:settings="data.options"></settings>
<div class="button-wrapper">
<button @click="submitSettings( 'ok' )" title="Save changes">Save</button>
<button @click="closePopup( 'cancel' )" title="Cancel changes">Cancel</button>
</div>
</div>
<div v-else-if="contentType === 'confirm'" class="confirm options">
<h3>{{ data.message }}</h3>
<div class="button-wrapper">
<button @click="closePopup( 'ok' )" title="Confirm operation">Ok</button>
<button @click="closePopup( 'cancel' )" title="Cancel operation">Cancel</button>
</div>
</div>
<div v-else-if="contentType === 'dropdown'" class="options">
<h3>{{ data.message }}</h3>
<select id="select" v-model="data.selected">
<option v-for="selectOption in data.options" :value="selectOption.value">{{ selectOption.displayName }}</option>
</select>
<div class="button-wrapper">
<button @click="closePopup( 'ok' )" title="Save changes">Save</button>
<button @click="closePopup( 'cancel' )" title="Cancel changes">Cancel</button>
</div>
</div>
<div v-else-if="contentType === 'selection'" class="options selection">
<h3>{{ data.message }}</h3>
<div v-for="selectOption in data.options">
<button class="select-button" @click="closePopupAdvanced( 'ok', selectOption.value )">{{ selectOption.displayName }}</button>
</div>
</div>
<div v-else-if="contentType === 'tickets'" class="options">
<h3>{{ data.message }}</h3>
<table>
<tr v-for="ticketOption in data.options.ageGroups">
<td>
{{ ticketOption.name }} <div style="display: inline" v-if="ticketOption.age">({{ ticketOption.age }})</div>
</td>
<td>
{{ data.options.currency }} {{ data.options.price[ ticketOption.id ] }}
</td>
<td>
<span class="material-symbols-outlined controls" @click="selectTicket( ticketOption.id )">add</span>
{{ data.options.count[ ticketOption.id ] }}
<span class="material-symbols-outlined controls" @click="deselectTicket( ticketOption.id )">remove</span>
</td>
</tr>
</table>
<div class="button-wrapper">
<button @click="submitTicket()" title="Save changes">Save</button>
<button @click="closePopup( 'cancel' )" title="Cancel changes">Cancel</button>
</div>
</div>
<div v-else-if="contentType === 'iframe'" class="options iframe-wrapper">
<iframe :src="data.options.link" frameborder="0" class="popup-iframe"></iframe>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- Options to be passed in: html, settings (for settings component), strings, confirm, dropdowns, selection -->
<script>
import settings from '@/components/settings/settings.vue';
export default {
name: 'popups',
components: {
settings,
},
props: {
size: {
type: String,
'default': 'normal',
},
},
data () {
return {
contentType: 'dropdown',
data: {},
info: '',
};
},
methods: {
closePopup( message ) {
if ( this.data.options.disallowedCharacters ) {
for ( let letter in this.data.selected ) {
if ( this.data.options.disallowedCharacters.includes( this.data.selected[ letter ] ) ) {
this.info = `Illegal character "${ this.data.selected[ letter ] }"`;
return false;
}
}
}
$( '#popup-backdrop' ).fadeOut( 300 );
if ( message ) {
this.$emit( 'data', { 'data': this.data.selected, 'status': message } );
}
},
selectTicket ( option ) {
let total = 0;
for ( let i in this.data.options.count ) {
total += this.data.options.count[ i ];
}
if ( total < this.data.options.max ) {
this.data.options.count[ option ] += 1;
}
},
deselectTicket ( option ) {
if ( this.data.options.count[ option ] > 0 ) {
this.data.options.count[ option ] -= 1;
}
},
submitTicket () {
$( '#popup-backdrop' ).fadeOut( 300 );
this.$emit( 'ticket', { 'data': this.data.options.count, 'component': this.data.options.id } );
},
closePopupAdvanced ( message, data ) {
this.data[ 'selected' ] = data;
this.closePopup( message );
},
submitSettings () {
$( '#popup-backdrop' ).fadeOut( 300 );
const dat = this.data.options;
let ret = {};
for ( let setting in dat ) {
if ( dat[ setting ][ 'type' ] !== 'link' ) {
ret[ setting ] = dat[ setting ][ 'value' ];
}
}
this.$emit( 'data', { 'data': ret, 'status': 'settings' } );
},
openPopup ( message, options, dataType, selected ) {
this.data = { 'message': message ?? 'No message defined on method call!!', 'options': options ?? { '1': { 'value': 'undefined', 'displayName': 'No options specified in call' } }, 'selected': selected ?? '' };
this.contentType = dataType ?? 'string';
$( '#popup-backdrop' ).fadeIn( 300 );
}
},
};
</script>
<style scoped>
#popup-backdrop {
position: fixed;
top: 0;
left: 0;
z-index: 10;
width: 100vw;
height: 100vh;
background-color: var( --overlay-color );
display: none;
}
.popup-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.button-wrapper {
width: 100%;
margin-top: 3%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
}
.close-wrapper {
width: 100%;
height: 5%;
display: flex;
justify-content: center;
align-items: flex-end;
flex-direction: column;
}
.close-button {
margin-right: 1vw;
margin-top: 2vw;
font-size: 200%;
cursor: pointer;
}
.popup {
border: none;
border-radius: 20px;
background-color: var( --popup-color );
width: 90vw;
height: 80vh;
}
.popup-iframe {
width: 100%;
height: 100%;
}
.iframe-wrapper {
height: 100%;
}
.message-container {
height: 90%;
width: 90%;
margin-left: 5%;
overflow: scroll;
}
.options {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
overflow: visible;
min-height: 100%;
width: 100%;
}
.options button {
padding: 1% 2%;
display: inline-block;
background-color: var( --accent-background );
color: var( --secondary-color );
cursor: pointer;
}
.options button:hover {
background-color: var( --accent-background-hover );
}
.select-button {
background-color: rgba( 0, 0, 0, 0 ) !important;
color: var( --primary-color ) !important;
padding: 1vh 2vw !important;
border: solid var( --primary-color ) 1px;
border-radius: 5px;
transition: all 0.5s ease-in-out;
margin-bottom: 1vh;
}
.select-button:hover {
background-color: var( --hover-color ) !important;
}
.controls {
user-select: none;
cursor: pointer;
font-size: 100%;
font-weight: bold;
border: solid var( --primary-color ) 1px;
border-radius: 100%;
}
@media only screen and (min-width: 999px) {
.small {
width: 40%;
height: 40%;
}
.normal {
width: 50%;
height: 50%;
}
.big {
width: 60%;
height: 60%;
}
.bigger {
width: 70%;
height: 70%;
}
.huge {
width: 80%;
height: 80%;
}
}
</style>

View File

@@ -1,335 +0,0 @@
<!-- eslint-disable no-undef -->
<!--
* libreevent - properties.vue
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div id="properties">
<h2>Properties</h2>
<h3>General settings</h3>
<table>
<tr>
<td>Zoom Factor:</td>
<td>
{{ Math.round( zoomFactor * 1000 ) / 1000 }}x
</td>
</tr>
<!-- <tr>
<td>Row naming scheme</td>
<td>
<select v-model="generalSettings[ 'namingScheme' ]">
<option value="numeric">Numeric</option>
<option value="alphabetic">Alphabetic</option>
<option value="roman">Roman numerals</option>
</select>
</td>
</tr> -->
</table>
<h3>Component settings</h3>
<table v-if="active">
<!-- H/W/Y/X for rendering -->
<tr>
<td>Position X:</td>
<td>
<input type="number" min="20" v-model="internal[ active ].x" @change="resubmit()">
</td>
</tr>
<tr>
<td>Position Y:</td>
<td>
<input type="number" min="20" v-model="internal[ active ].y" @change="resubmit()">
</td>
</tr>
<tr>
<td>Width:</td>
<td>
<input type="number" min="20" v-model="internal[ active ].w" @change="resubmit()">
</td>
</tr>
<tr>
<td>Height:</td>
<td>
<input type="number" min="20" v-model="internal[ active ].h" @change="resubmit()">
</td>
</tr>
<!-- Starting row for row counting and rendering -->
<tr v-if="internal[ active ].type == 'seat'">
<td>Starting row:</td>
<td>
<input type="number" min="1" max="20" v-model="internal[ active ].startingRow" @change="resubmit()">
</td>
</tr>
<!-- ORIGIN of component for rendering -->
<tr>
<td>Origin:</td>
<td>
<input type="number" min="1" max="4" v-model="internal[ active ].origin" @change="resubmit()">
</td>
</tr>
<!-- TEXT Settings -->
<tr v-if="internal[ active ].type == 'text'">
<td>Text:</td>
<td>
<input type="text" v-model="internal[ active ].text.text" @change="resubmit()">
</td>
</tr>
<tr v-if="internal[ active ].type == 'text'">
<td>Text Size:</td>
<td>
<input type="number" min="1" max="100" v-model="internal[ active ].text.textSize" @change="resubmit()">
</td>
</tr>
<tr v-if="internal[ active ].type == 'text'">
<td>Text colour:</td>
<td>
<input type="text" data-coloris v-model="internal[ active ].text.colour" onkeydown="return false;">
</td>
</tr>
<!-- CATEGORY -->
<tr v-if="internal[ active ].type == 'seat' || internal[ active ].type == 'stand'">
<td>Category:</td>
<td>
<input type="text" v-model="internal[ active ].category" @change="resubmit()">
</td>
</tr>
<!-- SECTOR -->
<tr v-if="internal[ active ].type == 'seat' || internal[ active ].type == 'stand'">
<td>Sector:</td>
<td>
<input type="text" min="1" max="4" v-model="internal[ active ].sector" @change="resubmit()">
</td>
</tr>
<!-- TICKET COUNT -->
<tr v-if="internal[ active ].type == 'stand'">
<td>Ticket count:</td>
<td>
<input type="number" min="1" max="4" v-model="internal[ active ].ticketCount" @change="resubmit()">
</td>
</tr>
<!-- SEAT NUMBERING for seats -->
<tr v-if="internal[ active ].type == 'seat'">
<td>Component number:
<div class="info-container" @mouseenter="showInfo( 'componentNumber' )" @mouseleave="hideInfo( 'componentNumber' )">
<span class="material-symbols-outlined info-icon">info</span>
<div class="info-box" id="componentNumber">
<div class="info-box-container">
<div>
With this you can change what the order of the components is which is used to determine the seat numbering. Read more <a href="https://libreevent.janishutz.com/docs/admin-panel/seatplan-editor#component-number-property">here</a>
</div>
</div>
</div>
</div>
</td>
<td>
<input type="number" min="1" max="100" v-model="internal[ active ].seatNumberingPosition" @change="resubmit()">
</td>
</tr>
<!-- SEAT NUMBERING direction -->
<tr v-if="internal[ active ].type == 'seat'">
<td>Numbering direction:</td>
<td>
<select min="20" v-model="internal[ active ].numberingDirection" @change="resubmit()">
<option value="left">Left to right</option>
<option value="right">Right to left</option>
</select>
</td>
</tr>
<!-- Component type selector -->
<tr>
<td>Type:</td>
<td>
<select min="20" v-model="internal[ active ].type" @change="resubmit()">
<option value="seat">Seat</option>
<option value="stand">Stand</option>
<option value="stage">Stage</option>
<option value="text">Text</option>
</select>
</td>
</tr>
<!-- SHAPE of component if not a text element -->
<tr v-if="internal[ active ].type != 'text'">
<td>Shape:</td>
<td>
<select min="20" v-model="internal[ active ].shape" @change="resubmit()">
<option value="rectangular">Rectangular</option>
<option value="trapezoid">Trapezoid</option>
<option value="circular">Circular</option>
</select>
</td>
</tr>
</table>
<div v-else class="no-select">
<b>No component selected</b><br>
Please select one to view details here.
</div>
</div>
</template>
<script>
export default {
name: 'propertiesSeatplan',
props: {
draggables: {
type: Object,
'default': {}
},
scaleFactor: {
type: Number,
'default': 1,
},
generalSettings: {
type: Object,
'default': {},
},
zoomFactor: {
type: Number,
'default': 1
},
active: {
type: Number,
'default': 1,
},
historyPos: {
type: Number,
'default': 0,
}
},
data () {
return {
internal: {},
categories: { '1': {} },
};
},
methods: {
loadInternal () {
for ( let value in this.draggables ) {
this.internal[ value ] = {};
for ( let info in this.draggables[ value ] ) {
if ( info === 'w' || info === 'h' || info === 'x' || info === 'y' ) {
this.internal[ value ][ info ] = Math.round( ( this.draggables[ value ][ info ] / this.scaleFactor ) * 1000 ) / 1000;
} else {
this.internal[ value ][ info ] = this.draggables[ value ][ info ];
}
}
}
},
resubmit () {
let ret = {};
for ( let value in this.internal ) {
ret[ value ] = {};
for ( let info in this.internal[ value ] ) {
if ( info === 'w' || info === 'h' || info === 'x' || info === 'y' ) {
ret[ value ][ info ] = this.internal[ value ][ info ] * this.scaleFactor;
} else {
ret[ value ][ info ] = this.internal[ value ][ info ];
}
}
}
this.$emit( 'updated', ret );
},
showInfo ( box ) {
$( '#' + box ).stop();
$( '#' + box ).fadeIn( 300 );
},
hideInfo ( box ) {
$( '#' + box ).stop();
$( '#' + box ).fadeOut( 300 );
}
},
watch: {
draggables () {
this.loadInternal();
},
active () {
this.loadInternal();
},
scaleFactor () {
this.loadInternal();
},
historyPos () {
this.loadInternal();
}
},
created () {
this.loadInternal();
}
};
</script>
<style scoped>
.info-container {
display: inline;
position: relative;
}
.info-icon {
font-size: 100%;
cursor: default;
}
.info-box {
color: var( --primary-color );
text-align: center;
display: none;
position: absolute;
z-index: 10;
width: 20vw;
height: 20vh;
background-color: var( --hint-color );
border-radius: 20px;
top: 125%;
right: -9.3vw;
}
.info-box::before {
content: " ";
position: absolute;
bottom: 100%; /* At the bottom of the tooltip */
left: 50%;
margin-left: -5px;
border-width: 10px;
border-style: solid;
border-color: transparent transparent var( --hint-color ) transparent;
}
.info-box-container {
display: flex;
width: 80%;
height: 80%;
padding: 10%;
padding-top: 5%;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -1,508 +0,0 @@
<!-- eslint-disable no-undef -->
<!--
* libreevent - window.vue
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div id="window">
<properties class="properties" v-model:draggables="draggables" @updated="handleUpdate" :scale-factor="scaleFactor" :active="active" :history-pos="historyPos" :zoom-factor="zoomFactor" v-model:general-settings="generalSettings"></properties>
<div class="parent" id="parent">
<div class="content-parent">
<Vue3DraggableResizable v-for="draggable in draggables" :initW="draggable.w" :initH="draggable.h" v-model:x="draggable.x" v-model:y="draggable.y" v-model:w="draggable.w" v-model:h="draggable.h"
v-model:active="draggable.active" v-model:draggable="draggable.draggable" :resizable="draggable.resizable" :parent="true" @activated="activateComponent( draggable.id );"
@drag-end="saveHistory();" @resize-end="saveHistory();" @contextmenu="( e ) => { e.preventDefault(); }" class="draggable-box">
<circularSeatplanComponent v-if="draggable.shape == 'circular' && draggable.type == 'seat'" :scale-factor="scaleFactor"
:w="draggable.w" :h="draggable.h" :origin="draggable.origin" :starting-row="draggable.startingRow" :id="draggable.id" @seatingInfo="( info ) => { handleSeatCountInfo( info ); }"></circularSeatplanComponent>
<trapezoidSeatplanComponent v-else-if="draggable.shape == 'trapezoid' && draggable.type == 'seat'" :scale-factor="scaleFactor"
:w="draggable.w" :h="draggable.h" :origin="draggable.origin" :starting-row="draggable.startingRow" :id="draggable.id" @seatingInfo="( info ) => { handleSeatCountInfo( info ); }"></trapezoidSeatplanComponent>
<rectangularSeatplanComponent v-else-if="draggable.shape == 'rectangular' && draggable.type == 'seat'" :scale-factor="scaleFactor"
:w="draggable.w" :h="draggable.h" :origin="draggable.origin" :id="draggable.id" @seatingInfo="( info ) => { handleSeatCountInfo( info ); }"></rectangularSeatplanComponent>
<stagesSeatplanComponent v-else-if="draggable.type == 'stage'" :origin="draggable.origin" :shape="draggable.shape"></stagesSeatplanComponent>
<standingSeatplanComponent v-else-if="draggable.type == 'stand'" :origin="draggable.origin" :shape="draggable.shape"></standingSeatplanComponent>
<textFieldSeatplanComponent v-else-if="draggable.type == 'text'" :text="draggable.text.text" :text-size="draggable.text.textSize"
:colour="draggable.text.colour" :origin="draggable.origin" :scale-factor="scaleFactor"></textFieldSeatplanComponent>
</Vue3DraggableResizable>
</div>
</div>
<div class="toolbar">
<button title="Go back to location settings" @click="this.$router.push( '/admin/locations' )"><span class="material-symbols-outlined">arrow_back</span></button>
<button title="Undo [Ctrl + Z]" v-if="available.undo" @click="historyOp( 'undo' )"><span class="material-symbols-outlined">undo</span></button>
<button title="Undo (unavailable)" v-else disabled><span class="material-symbols-outlined">undo</span></button>
<button title="Redo [Ctrl + Y]" v-if="available.redo" @click="historyOp( 'redo' )"><span class="material-symbols-outlined">redo</span></button>
<button title="Redo (unavailable)" v-else disabled><span class="material-symbols-outlined">redo</span></button>
<button title="Zoom in [+]" @click="zoom( 0.2 )"><span class="material-symbols-outlined">zoom_in</span></button>
<button title="Reset zoom [=]" @click="zoom( 1 );"><span class="material-symbols-outlined">center_focus_strong</span></button>
<button title="Zoom out [-]" @click="zoom( -0.2 )"><span class="material-symbols-outlined">zoom_out</span></button>
<button title="Add component [Ctrl + I]" @click="addNewElement()"><span class="material-symbols-outlined">add</span></button>
<button title="Remove selected component [Delete]" @click="deleteSelected()"><span class="material-symbols-outlined">delete</span></button>
<button title="Save this seatplan as a draft [Ctrl + S]" @click="saveDraft()"><span class="material-symbols-outlined">save</span></button>
<button title="Deploy this seatplan (save it for use)" @click="deploy()"><span class="material-symbols-outlined">system_update_alt</span></button>
</div>
<notifications ref="notification" location="topleft"></notifications>
</div>
</template>
<script>
import Vue3DraggableResizable from 'vue3-draggable-resizable';
import properties from '@/components/seatplan/editor/properties.vue';
import circularSeatplanComponent from '@/components/seatplan/seatplanComponents/seats/circular.vue';
import rectangularSeatplanComponent from '@/components/seatplan/seatplanComponents/seats/rectangular.vue';
import trapezoidSeatplanComponent from '@/components/seatplan/seatplanComponents/seats/trapezoid.vue';
import stagesSeatplanComponent from '@/components/seatplan/seatplanComponents/stages.vue';
import standingSeatplanComponent from '@/components/seatplan/seatplanComponents/standing.vue';
import textFieldSeatplanComponent from '@/components/seatplan/seatplanComponents/textField.vue';
import notifications from '@/components/notifications/notifications.vue';
import 'vue3-draggable-resizable/dist/Vue3DraggableResizable.css';
export default {
'name': 'window',
components: {
Vue3DraggableResizable,
properties,
circularSeatplanComponent,
rectangularSeatplanComponent,
trapezoidSeatplanComponent,
stagesSeatplanComponent,
standingSeatplanComponent,
textFieldSeatplanComponent,
notifications,
},
data() {
return {
active: 0,
draggables: { 1: { 'x': 100, 'y': 100, 'h': 100, 'w': 250, 'active': false, 'draggable': true, 'resizable': true, 'id': 1, 'origin': 1, 'shape': 'rectangular', 'type': 'seat', 'startingRow': 1, 'sector': 'A', 'text': { 'text': 'TestText', 'textSize': 20, 'colour': '#20FFFF' }, 'numberingDirection': 'left', 'seatNumberingPosition': 1, 'category': '1' } },
available: { 'redo': false, 'undo': false },
scaleFactor: 1,
sizePoll: null,
prevSize: { 'h': window.innerHeight, 'w': window.innerWidth },
zoomFactor: 1,
historyPos: 0,
generalSettings: { 'namingScheme': 'numeric' },
seatCountInfo: { 'details': {}, 'count': 0 },
autoSave: null,
};
},
methods: {
/*
Coords are from top left corner of box.
The below function is executed as the init hook (created hook)
of vue.js, so whenever this particular page is loaded.
It loads previous data (if available) and starts the event listeners
for keyevents (like delete) and also check if the user uses a desktop
browser that meets all the requirements for being able to use the editor
reliably according to testing done.
*/
runHook () {
if ( !sessionStorage.getItem( 'locationID' ) ) {
this.$router.push( '/admin/events' );
}
let self = this;
this.zoomFactor = sessionStorage.getItem( 'zoom' ) ? parseFloat( sessionStorage.getItem( 'zoom' ) ) : 1;
/*
Keybinds:
- Delete: delete selected object
- Ctrl + S: Save
- Ctrl + Z: Undo
- Ctrl + Y: Redo
*/
document.onkeydown = function ( event ) {
if ( event.key === 'Delete' ) {
event.preventDefault();
self.deleteSelected();
} else if ( event.ctrlKey && event.key === 's' ) {
event.preventDefault();
self.saveDraft();
} else if ( ( event.ctrlKey && event.key === 'y' ) ) {
event.preventDefault();
self.historyOp( 'redo' );
} else if ( event.ctrlKey && event.key === 'z' ) {
event.preventDefault();
self.historyOp( 'undo' );
} else if ( event.ctrlKey && event.key === 'i' ) {
event.preventDefault();
self.addNewElement();
} else if ( event.key === '+' ) {
self.zoom( 0.2 );
} else if ( event.key === '-' ) {
self.zoom( -0.2 );
} else if ( event.key === '=' ) {
self.zoom( 1 );
}
};
// Auto save every 60s (60K ms)
this.autoSave = setInterval( () => {
const options = {
method: 'post',
body: JSON.stringify( { 'data': { 'seatInfo': this.seatCountInfo, 'data': this.scaleDown( this.draggables ) }, 'location': sessionStorage.getItem( 'locationID' ) } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/admin/api/saveSeatplanDraft', options );
}, 60000 );
/*
Calculate scale factor (this adds support for differently sized screens)
900px is the "default" height
*/
let height = $( document ).height() * 0.8;
this.scaleFactor = ( height / 900 ) * this.zoomFactor;
/*
Load seatplan
*/
fetch( localStorage.getItem( 'url' ) + '/admin/getAPI/getSeatplanDraft?location=' + sessionStorage.getItem( 'locationID' ) ).then( res => {
if ( res.status === 200 ) {
res.json().then( data => {
this.draggables = this.scaleUp( data.data );
sessionStorage.setItem( 'seatplan', JSON.stringify( data.data ) );
for ( let element in this.draggables ) {
if ( this.draggables[ element ].active ) {
this.draggables[ element ].active = false;
}
}
} );
} else if ( res.status === 500 ) {
if ( sessionStorage.getItem( 'seatplan' ) ) {
this.draggables = this.scaleUp( JSON.parse( sessionStorage.getItem( 'seatplan' ) ) );
}
for ( let element in this.draggables ) {
if ( this.draggables[ element ].active ) {
this.draggables[ element ].active = false;
}
}
}
} );
if ( !sessionStorage.getItem( 'seatplan-history' ) ) {
sessionStorage.setItem( 'seatplan-history', JSON.stringify( { '1': this.scaleDown( this.draggables ) } ) );
}
let history = sessionStorage.getItem( 'seatplan-history' ) ? JSON.parse( sessionStorage.getItem( 'seatplan-history' ) ) : {};
let count = parseInt( Object.keys( history ).length );
if ( count > parseInt( sessionStorage.getItem( 'historyPos' ) ) ) {
this.available.redo = true;
}
if ( parseInt( sessionStorage.getItem( 'historyPos' ) ) > 0 ) {
this.available.undo = true;
}
// if ( window.webpage.engine.trident ) {
// alert( 'Welcome! We have detected that you are still using Internet Explorer or a similar browser. As a modern webapp, libreevent does NOT officially support Internet Explorer. If you run into problems whilst using this webapp, please switch to a modern browser like Firefox.' );
// } else if ( window.webpage.engine.presto ) {
// alert( 'Welcome! We have detected that you are a very old version of Opera or related browser. As a modern webapp, libreevent does only support modern browsers. If you run into issues whilst using this webapp, please switch to a modern browser, like Firefox.' );
// } else if ( window.webpage.engine.webkit ) {
// alert( 'Hello! Whilst tested with some versions of Webkit (the browser engine of Safari), support for this engine is still unofficial. Therefore we cannot guarantee that all the features of the seatplan editor function as they should. If you run into problems, please contact us through the link provided in the documentation.' );
// }
this.save();
},
eventHandler () {
if ( this.prevSize.h != window.innerHeight || this.prevSize.w != window.innerWidth ) {
this.prevSize = { 'h': window.innerHeight, 'w': window.innerWidth };
this.loadSeatplan();
}
},
loadSeatplan () {
/*
Calculate scale factor (this adds support for differently sized screens)
900px is the "default" height
*/
let height = $( document ).height() * 0.8;
this.scaleFactor = ( height / 900 ) * this.zoomFactor;
if ( sessionStorage.getItem( 'seatplan' ) ) {
this.draggables = this.scaleUp( JSON.parse( sessionStorage.getItem( 'seatplan' ) ) );
}
for ( let element in this.draggables ) {
if ( this.draggables[ element ].active ) {
this.draggables[ element ].active = false;
}
}
},
scaleDown ( valueArray ) {
const allowedAttributes = [ 'w', 'h', 'x', 'y' ];
let returnArray = {};
for ( let entry in valueArray ) {
returnArray[ entry ] = {};
for ( let attributes in valueArray[ entry ] ) {
if ( allowedAttributes.includes( attributes ) ) {
returnArray[ entry ][ attributes ] = Math.round( ( valueArray[ entry ][ attributes ] / this.scaleFactor ) * 1000 ) / 1000;
} else {
returnArray[ entry ][ attributes ] = valueArray[ entry ][ attributes ];
}
}
}
return returnArray;
},
scaleUp ( valueArray ) {
const allowedAttributes = [ 'w', 'h', 'x', 'y' ];
let returnArray = {};
for ( let entry in valueArray ) {
returnArray[ entry ] = {};
for ( let attributes in valueArray[ entry ] ) {
if ( allowedAttributes.includes( attributes ) ) {
returnArray[ entry ][ attributes ] = Math.round( ( valueArray[ entry ][ attributes ] * this.scaleFactor ) * 1000 ) / 1000;
} else {
returnArray[ entry ][ attributes ] = valueArray[ entry ][ attributes ];
}
}
}
return returnArray;
},
activateComponent ( id ) {
this.active = id;
},
saveHistory () {
let history = sessionStorage.getItem( 'seatplan-history' ) ? JSON.parse( sessionStorage.getItem( 'seatplan-history' ) ) : {};
let count = parseInt( Object.keys( history ).length + 1 );
this.historyPos = count;
if ( count - 1 > parseInt( sessionStorage.getItem( 'historyPos' ) ) ) {
for ( let i = parseInt( sessionStorage.getItem( 'historyPos' ) ) + 1; i < count; i++ ) {
delete history[ i ];
this.available.redo = false;
}
}
count = parseInt( Object.keys( history ).length + 1 );
sessionStorage.setItem( 'historyPos', count );
history[ count ] = this.scaleDown( this.draggables );
sessionStorage.setItem( 'seatplan-history', JSON.stringify( history ) );
if ( parseInt( sessionStorage.getItem( 'historyPos' ) ) > 1 ) {
this.available.undo = true;
}
this.save();
},
historyOp ( action ) {
if ( action === 'undo' ) {
if ( parseInt( sessionStorage.getItem( 'historyPos' ) ) > 1 ) {
sessionStorage.setItem( 'historyPos', parseInt( sessionStorage.getItem( 'historyPos' ) ) - 1 );
this.draggables = this.scaleUp( JSON.parse( sessionStorage.getItem( 'seatplan-history' ) )[ sessionStorage.getItem( 'historyPos' ) ] );
this.available.redo = true;
if ( parseInt( sessionStorage.getItem( 'historyPos' ) ) < 2 ) {
this.available.undo = false;
}
} else {
this.available.undo = false;
}
} else if ( action === 'redo' ) {
if ( parseInt( Object.keys( JSON.parse( sessionStorage.getItem( 'seatplan-history' ) ) ).length ) > parseInt( sessionStorage.getItem( 'historyPos' ) ) ) {
sessionStorage.setItem( 'historyPos', parseInt( sessionStorage.getItem( 'historyPos' ) ) + 1 );
this.draggables = this.scaleUp( JSON.parse( sessionStorage.getItem( 'seatplan-history' ) )[ sessionStorage.getItem( 'historyPos' ) ] );
this.available.undo = true;
if ( parseInt( Object.keys( JSON.parse( sessionStorage.getItem( 'seatplan-history' ) ) ).length ) < parseInt( sessionStorage.getItem( 'historyPos' ) ) + 1 ) {
this.available.redo = false;
}
} else {
this.available.redo = false;
}
}
this.historyPos = sessionStorage.getItem( 'historyPos' );
},
save () {
sessionStorage.setItem( 'seatplan', JSON.stringify( this.scaleDown( this.draggables ) ) );
},
saveDraft () {
if ( !this.getSeatCount() ) {
this.$refs.notification.createNotification( 'Collision of seat count!', 10, 'error', 'normal' );
return;
}
let progressNotification = this.$refs.notification.createNotification( 'Saving as draft', 5, 'progress', 'normal' );
sessionStorage.setItem( 'seatplan', JSON.stringify( this.scaleDown( this.draggables ) ) );
const options = {
method: 'post',
body: JSON.stringify( { 'data': { 'seatInfo': this.seatCountInfo, 'data': this.scaleDown( this.draggables ) }, 'location': sessionStorage.getItem( 'locationID' ) } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/admin/api/saveSeatplanDraft', options ).then( res => {
if ( res.status === 200 ) {
res.text().then( () => {
this.$refs.notification.cancelNotification( progressNotification );
this.$refs.notification.createNotification( 'Saved as draft', 5, 'ok', 'normal' );
} );
} else if ( res.status === 403 ) {
this.$refs.notification.cancelNotification( progressNotification );
this.$refs.notification.createNotification( 'Unauthenticated', 5, 'ok', 'error' );
}
} );
},
deploy () {
if ( !this.getSeatCount() ) {
this.$refs.notification.createNotification( 'Collision of seat count!', 10, 'error', 'normal' );
return;
}
let deployNotification = this.$refs.notification.createNotification( 'Deploying...', 5, 'progress', 'normal' );
const options = {
method: 'post',
body: JSON.stringify( { 'data': { 'seatInfo': this.seatCountInfo, 'data': this.scaleDown( this.draggables ) }, 'location': sessionStorage.getItem( 'locationID' ) } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/admin/api/saveSeatplan', options ).then( res => {
if ( res.status === 200 ) {
res.text().then( () => {
this.$refs.notification.cancelNotification( deployNotification );
this.$refs.notification.createNotification( 'Deployed successfully', 5, 'ok', 'normal' );
} );
} else if ( res.status === 403 ) {
this.$refs.notification.cancelNotification( deployNotification );
this.$refs.notification.createNotification( 'Unauthenticated', 5, 'ok', 'error' );
}
} );
},
addNewElement () {
this.draggables[ Object.keys( this.draggables ).length + 1 ] = { 'x': 100, 'y': 100, 'h': 100, 'w': 250, 'active': false, 'draggable': true, 'resizable': true, 'id': ( Object.keys( this.draggables ).length + 1 ), 'origin': 1, 'shape': 'rectangular', 'type': 'seat', 'startingRow': 1, 'seatNumberingPosition': Object.keys( this.draggables ).length, 'sector': 'A', 'text': { 'text': 'TestText', 'textSize': 20, 'colour': '#20FFFF' }, 'ticketCount': 1, 'numberingDirection': 'left', 'category': '1' };
this.saveHistory();
document.getElementById( 'parent' ).scrollTop = 0;
document.getElementById( 'parent' ).scrollLeft = 0;
this.$refs.notification.createNotification( 'New component added successfully', 5, 'ok', 'normal' );
},
deleteSelected () {
if ( this.active ) {
this.draggables[ this.active ].active = true;
if ( confirm( 'Do you really want to delete the selected item?' ) ) {
delete this.draggables[ this.active ];
this.saveHistory();
this.active = 0;
this.$refs.notification.createNotification( 'Successfully deleted component', 5, 'ok', 'normal' );
}
} else {
this.$refs.notification.createNotification( 'Please select a seat first!', 5, 'error', 'normal' );
}
},
handleUpdate ( value ) {
this.draggables = value;
this.selectedObject = value;
this.saveHistory();
},
zoom ( scale ) {
if ( scale == 1 ) {
this.zoomFactor = 1;
sessionStorage.setItem( 'zoom', this.zoomFactor );
this.loadSeatplan();
} else {
if ( ( this.zoomFactor < 0.3 && scale < 0 ) || ( this.zoomFactor > 2.9 && scale > 0 ) ) {
if ( this.zoomFactor < 0.3 ) {
this.$refs.notification.createNotification( 'Minimum zoom factor reached', 5, 'warning', 'normal' );
} else {
this.$refs.notification.createNotification( 'Maximum zoom factor reached', 5, 'warning', 'normal' );
}
} else {
this.zoomFactor += scale;
}
sessionStorage.setItem( 'zoom', this.zoomFactor );
this.loadSeatplan();
}
},
handleSeatCountInfo ( info ) {
this.seatCountInfo[ 'details' ][ info.id ] = info.data;
this.seatCountInfo[ 'details' ][ info.id ][ 'startingRow' ] = this.draggables[ info.id ].startingRow;
},
getSeatCount () {
this.seatCountInfo[ 'count' ] = document.getElementsByClassName( 'seats' ).length;
for ( let draggable in this.draggables ) {
if ( this.draggables[ draggable ][ 'ticketCount' ] ) {
this.seatCountInfo[ 'count' ] += this.draggables[ draggable ][ 'ticketCount' ];
}
}
// Remap seat count info
this.seatCountInfo[ 'data' ] = {};
for ( let element in this.seatCountInfo[ 'details' ] ) {
if ( !this.seatCountInfo[ 'data' ][ this.draggables[ 1 ].sector ] ) {
this.seatCountInfo[ 'data' ][ this.draggables[ 1 ].sector ] = {};
}
if ( this.seatCountInfo[ 'data' ][ this.draggables[ 1 ].sector ][ this.draggables[ element ].seatNumberingPosition ] ) return false;
this.seatCountInfo[ 'data' ][ this.draggables[ 1 ].sector ][ this.draggables[ element ].seatNumberingPosition ] = this.seatCountInfo[ 'details' ][ element ];
}
return true;
},
},
created () {
this.runHook();
this.sizePoll = setInterval( this.eventHandler, 250 );
},
unmounted() {
clearInterval( this.sizePoll );
clearInterval( this.autoSave );
},
};
</script>
<style scoped>
.parent {
height: 90vh;
aspect-ratio: 16 / 9;
top: 7.5vh;
left: 3vw;
position: absolute;
border: black 1px solid;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
overflow: scroll;
}
.draggable-box {
cursor: all-scroll;
}
.properties {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
z-index: 1;
background-color: var( --accent-background );
color: var( --secondary-color );
width: 20vw;
height: 90vh;
top: 7.5vh;
right: 0.5vw;
overflow: scroll;
}
.content-parent {
aspect-ratio: 16 / 9;
height: 400%;
}
.toolbar {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: absolute;
top: 7.5vh;
left: 0.5vw;
height: 90vh;
}
.toolbar button {
margin-top: 10%;
cursor: pointer;
}
.toolbar button:disabled {
cursor: default;
}
</style>

View File

@@ -1,119 +0,0 @@
<!--
* libreevent - properties.vue
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div id="circularSeatplan">
<div v-for="row in seats">
<span class="material-symbols-outlined seats" v-for="seat in row" :style="seat.style">living</span>
</div>
</div>
</template>
<style scoped>
.seats {
position: absolute;
}
</style>
<script>
export default {
name: 'circularSeatplanComponent',
props: {
h: {
type: Number,
'default': 100,
},
w: {
type: Number,
'default': 200,
},
scaleFactor: {
type: Number,
'default': 1,
},
origin: {
type: Number,
'default': 1,
},
startingRow: {
type: Number,
'default': 1,
},
id: {
type: Number,
'default': 1
}
},
data () {
return {
seats: {},
};
},
methods: {
calculateChairs () {
// Size of seat at scale 1 is 32px
// w & h are normalised
let w = Math.round( this.w / this.scaleFactor );
let h = Math.round( this.h / this.scaleFactor );
const size = 33;
let count = Math.min( Math.floor( w / size ), Math.floor( h / size ) );
this.seats = {};
let details = { 'data': {}, 'id': this.id };
for ( let row = this.startingRow; row < count; row++ ) {
let nn = row * ( Math.PI / 2 );
details.data[ row - 1 ] = Math.ceil( nn );
let r = row * size;
this.seats[ row ] = {};
for ( let n = 0; n < nn; n++ ) {
let phi = n * size / ( row * size );
if ( this.origin === 1 ) {
this.seats[ row ][ n ] = { 'style': `font-size: ${this.scaleFactor * 200}%; bottom: ${ r * Math.cos( phi ) * this.scaleFactor }px; left: ${ r * Math.sin( phi ) * this.scaleFactor }px; rotate: ${ phi }rad` };
} else if ( this.origin === 2 ) {
this.seats[ row ][ n ] = { 'style': `font-size: ${this.scaleFactor * 200}%; bottom: ${ r * Math.cos( phi ) * this.scaleFactor }px; right: ${ r * Math.sin( phi ) * this.scaleFactor }px; rotate: ${ Math.PI * 2 - phi }rad` };
} else if ( this.origin === 3 ) {
this.seats[ row ][ n ] = { 'style': `font-size: ${this.scaleFactor * 200}%; top: ${ r * Math.cos( phi ) * this.scaleFactor }px; right: ${ r * Math.sin( phi ) * this.scaleFactor }px; rotate: ${ phi + Math.PI }rad` };
} else if ( this.origin === 4 ) {
this.seats[ row ][ n ] = { 'style': `font-size: ${this.scaleFactor * 200}%; top: ${ r * Math.cos( phi ) * this.scaleFactor }px; left: ${ r * Math.sin( phi ) * this.scaleFactor }px; rotate: ${ Math.PI - phi }rad` };
}
}
}
this.$emit( 'seatingInfo', details );
},
setScaleFactor () {
for ( let row in this.seats ) {
for ( let seat in this.seats[ row ] ) {
let styles = this.seats[ row ][ seat ].style.substring( this.seats[ row ][ seat ].style.indexOf( ';' ) + 1 );
this.seats[ row ][ seat ].style = `font-size: ${this.scaleFactor * 200}%;` + styles;
}
}
}
},
watch: {
scaleFactor() {
this.setScaleFactor();
},
h() {
this.calculateChairs();
},
w() {
this.calculateChairs();
},
origin() {
this.calculateChairs();
},
startingRow() {
this.calculateChairs();
}
},
created() {
this.calculateChairs();
}
};
</script>

View File

@@ -1,109 +0,0 @@
<!--
* libreevent - properties.vue
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div id="rectangularSeatplan">
<div v-for="row in seats" class="rows">
<span class="material-symbols-outlined seats" v-for="seat in row" :style="seat.style">living</span>
</div>
</div>
</template>
<style scoped>
.seats {
position: absolute;
}
</style>
<script>
export default {
name: 'rectangularSeatplanComponent',
props: {
h: {
type: Number,
'default': 100,
},
w: {
type: Number,
'default': 200,
},
scaleFactor: {
type: Number,
'default': 1,
},
origin: {
type: Number,
'default': 1,
},
id: {
type: Number,
'default': 1
}
},
data () {
return {
seats: {},
};
},
methods: {
calculateChairs () {
// Size of seat at scale 1 is 32px
// w & h are normalised
let w = Math.floor( this.w / this.scaleFactor );
let h = Math.floor( this.h / this.scaleFactor );
const size = 33;
this.seats = {};
let details = { 'data': {}, 'id': this.id };
for ( let row = 0; row < Math.floor( h / size ); row++ ) {
this.seats[ row ] = {};
details.data[ row ] = Math.floor( w / size );
for ( let n = 0; n < Math.floor( w / size ); n++ ) {
if ( this.origin === 1 ) {
this.seats[ row ][ n ] = { 'style': `font-size: ${this.scaleFactor * 200}%; bottom: ${ row * size * this.scaleFactor }px; left: ${ n * size * this.scaleFactor }px; rotate: ${ this.origin / 4 - 0.25 }turn;` };
} else if ( this.origin === 2 ) {
this.seats[ row ][ n ] = { 'style': `font-size: ${this.scaleFactor * 200}%; bottom: ${ row * size * this.scaleFactor }px; right: ${ n * size * this.scaleFactor }px; rotate: ${ this.origin / 4 - 0.25 }turn;` };
} else if ( this.origin === 3 ) {
this.seats[ row ][ n ] = { 'style': `font-size: ${this.scaleFactor * 200}%; top: ${ row * size * this.scaleFactor }px; right: ${ n * size * this.scaleFactor }px; rotate: ${ this.origin / 4 - 0.25 }turn;` };
} else if ( this.origin === 4 ) {
this.seats[ row ][ n ] = { 'style': `font-size: ${this.scaleFactor * 200}%; top: ${ row * size * this.scaleFactor }px; left: ${ n * size * this.scaleFactor }px; rotate: ${ this.origin / 4 - 0.25 }turn;` };
}
}
}
this.$emit( 'seatingInfo', details );
},
setScaleFactor () {
for ( let row in this.seats ) {
for ( let seat in this.seats[ row ] ) {
let styles = this.seats[ row ][ seat ].style.substring( this.seats[ row ][ seat ].style.indexOf( ';' ) + 1 );
this.seats[ row ][ seat ].style = `font-size: ${this.scaleFactor * 200}%;` + styles;
}
}
}
},
watch: {
scaleFactor() {
this.setScaleFactor();
},
h() {
this.calculateChairs();
},
w() {
this.calculateChairs();
},
origin() {
this.calculateChairs();
}
},
created() {
this.calculateChairs();
}
};
</script>

View File

@@ -1,122 +0,0 @@
<!--
* libreevent - properties.vue
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div id="trapezoidSeatplan">
<div v-for="row in seats">
<span class="material-symbols-outlined seats" v-for="seat in row" :style="seat.style">living</span>
</div>
</div>
</template>
<style scoped>
.seats {
position: absolute;
}
</style>
<script>
export default {
name: 'trapezoidSeatplanComponent',
props: {
h: {
type: Number,
'default': 100,
},
w: {
type: Number,
'default': 200,
},
scaleFactor: {
type: Number,
'default': 1,
},
origin: {
type: Number,
'default': 1,
},
startingRow: {
type: Number,
'default': 1,
},
id: {
type: Number,
'default': 1
}
},
data () {
return {
seats: {},
};
},
methods: {
calculateChairs () {
// Size of seat at scale 1 is 32px
// w & h are normalised
let w = Math.round( this.w / this.scaleFactor );
let h = Math.round( this.h / this.scaleFactor );
const size = 33;
let side = Math.min( w, h ) + 20;
let heightTriangle = Math.floor( Math.sqrt( side ** 2 - ( Math.sqrt( side ** 2 * 2 ) / 2 ) ) );
let sideOffset = size / Math.sqrt( 2 );
let count = Math.floor( heightTriangle / ( sideOffset * 2 ) );
const angle = Math.PI / 4;
this.seats = {};
let details = { 'data': {}, 'id': this.id };
for ( let row = this.startingRow; row < count; row++ ) {
let nn = 2 + ( row - 1 ) * 2;
this.seats[ row ] = {};
details.data[ row - 1 ] = Math.floor( nn );
for ( let n = 0; n < nn; n++ ) {
let side = n * sideOffset;
if ( this.origin === 1 ) {
this.seats[ row ][ n ] = { 'style': `font-size: ${this.scaleFactor * 200}%; bottom: ${ ( side + 5 ) * this.scaleFactor }px; left: ${ ( row * sideOffset * 2 - side ) * this.scaleFactor }px; rotate: ${ angle }rad` };
} else if ( this.origin === 2 ) {
this.seats[ row ][ n ] = { 'style': `font-size: ${this.scaleFactor * 200}%; bottom: ${ ( side + 5 ) * this.scaleFactor }px; right: ${ ( row * sideOffset * 2 - side ) * this.scaleFactor }px; rotate: ${ Math.PI * 2 - angle }rad` };
} else if ( this.origin === 3 ) {
this.seats[ row ][ n ] = { 'style': `font-size: ${this.scaleFactor * 200}%; top: ${ ( side + 5 ) * this.scaleFactor }px; right: ${ ( row * sideOffset * 2 - side ) * this.scaleFactor }px; rotate: ${ angle + Math.PI }rad` };
} else if ( this.origin === 4 ) {
this.seats[ row ][ n ] = { 'style': `font-size: ${this.scaleFactor * 200}%; top: ${ ( side + 5 ) * this.scaleFactor }px; left: ${ ( row * sideOffset * 2 - side ) * this.scaleFactor }px; rotate: ${ Math.PI - angle }rad` };
}
}
}
this.$emit( 'seatingInfo', details );
},
setScaleFactor () {
for ( let row in this.seats ) {
for ( let seat in this.seats[ row ] ) {
let styles = this.seats[ row ][ seat ].style.substring( this.seats[ row ][ seat ].style.indexOf( ';' ) + 1 );
this.seats[ row ][ seat ].style = `font-size: ${this.scaleFactor * 200}%;` + styles;
}
}
}
},
watch: {
scaleFactor() {
this.setScaleFactor();
},
h() {
this.calculateChairs();
},
w() {
this.calculateChairs();
},
origin() {
this.calculateChairs();
},
startingRow() {
this.calculateChairs();
}
},
created() {
this.calculateChairs();
}
};
</script>

View File

@@ -1,114 +0,0 @@
<!--
* libreevent - stages.vue
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div id="stages" class="stages">
<div id="rectangular" v-if="shape == 'rectangular'" class="stages" :style="style"></div>
<div id="trapezoid" v-else-if="shape == 'trapezoid'" class="stages"><div id="trapezoid-ingredient" :style="trapezoidStyle"><div id="trapezoid-line"></div></div></div>
<div id="circular" v-else-if="shape == 'circular'" class="stages"><div id="circular-ingredient" :style="circularStyle"></div></div>
</div>
</template>
<style scoped>
.stages {
height: 100%;
width: 100%;
}
#rectangular {
border-color: black;
border-width: 2px;
background-color: var( --popup-color );
}
#circular, #trapezoid {
overflow: hidden;
}
#trapezoid-ingredient {
display: flex;
justify-content: center;
align-items: flex-end;
height: 200%;
width: 200%;
position: relative;
bottom: 50%;
right: 50%;
}
#trapezoid-line {
border: black solid 1px;
height: 50%;
width: 100%;
display: block;
background-color: var( --popup-color );
}
#circular-ingredient {
border: solid black 2px;
border-radius: 100%;
height: 199%;
width: 199%;
position: relative;
background-color: var( --popup-color );
}
</style>
<script>
export default {
name: 'stagesSeatplanComponent',
props: {
origin: {
type: Number,
'default': 1,
},
shape: {
type: String,
'default': 'rectangular',
},
},
data() {
return {
style: 'border-style: none none solid none',
circularStyle: 'top: 0; left 100%;',
trapezoidStyle: 'rotate: 45deg',
};
},
methods: {
updateOrigin () {
if ( this.origin === 1 ) {
this.style = 'border-style: none none solid none';
this.circularStyle = 'top: 0; right: 100%;';
this.trapezoidStyle = 'rotate: 45deg';
} else if ( this.origin === 2 ) {
this.style = 'border-style: none solid none none';
this.circularStyle = 'top: 0; right: 0;';
this.trapezoidStyle = 'rotate: 135deg';
} else if ( this.origin === 3 ) {
this.style = 'border-style: solid none none none';
this.circularStyle = 'top: -100%; right: 0;';
this.trapezoidStyle = 'rotate: 225deg';
} else if ( this.origin === 4 ) {
this.style = 'border-style: none none none solid';
this.circularStyle = 'top: -100%; right: 100%;';
this.trapezoidStyle = 'rotate: 315deg';
}
}
},
watch: {
origin ( value ) {
this.updateOrigin();
}
},
created() {
this.updateOrigin();
}
};
</script>

View File

@@ -1,114 +0,0 @@
<!--
* libreevent - stages.vue
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div id="stages" class="stages">
<div id="rectangular" v-if="shape == 'rectangular'" class="stages" :style="style"></div>
<div id="trapezoid" v-else-if="shape == 'trapezoid'" class="stages"><div id="trapezoid-ingredient" :style="trapezoidStyle"><div id="trapezoid-line"></div></div></div>
<div id="circular" v-else-if="shape == 'circular'" class="stages"><div id="circular-ingredient" :style="circularStyle"></div></div>
</div>
</template>
<style scoped>
.stages {
height: 100%;
width: 100%;
}
#rectangular {
border-color: black;
border-width: 2px;
background-color: var( --popup-color );
}
#circular, #trapezoid {
overflow: hidden;
}
#trapezoid-ingredient {
display: flex;
justify-content: center;
align-items: flex-end;
height: 200%;
width: 200%;
position: relative;
bottom: 50%;
right: 50%;
}
#trapezoid-line {
border: black solid 1px;
height: 50%;
width: 100%;
display: block;
background-color: var( --popup-color );
}
#circular-ingredient {
border: solid black 2px;
border-radius: 100%;
height: 199%;
width: 199%;
position: relative;
background-color: var( --popup-color );
}
</style>
<script>
export default {
name: 'stagesSeatplanComponent',
props: {
origin: {
type: Number,
'default': 1,
},
shape: {
type: String,
'default': 'rectangular',
},
},
data() {
return {
style: 'border-style: none none solid none',
circularStyle: 'top: 0; left 100%;',
trapezoidStyle: 'rotate: 45deg',
};
},
methods: {
updateOrigin () {
if ( this.origin === 1 ) {
this.style = 'border-style: none none solid none';
this.circularStyle = 'top: 0; right: 100%;';
this.trapezoidStyle = 'rotate: 45deg';
} else if ( this.origin === 2 ) {
this.style = 'border-style: none solid none none';
this.circularStyle = 'top: 0; right: 0;';
this.trapezoidStyle = 'rotate: 135deg';
} else if ( this.origin === 3 ) {
this.style = 'border-style: solid none none none';
this.circularStyle = 'top: -100%; right: 0;';
this.trapezoidStyle = 'rotate: 225deg';
} else if ( this.origin === 4 ) {
this.style = 'border-style: none none none solid';
this.circularStyle = 'top: -100%; right: 100%;';
this.trapezoidStyle = 'rotate: 315deg';
}
}
},
watch: {
origin () {
this.updateOrigin();
}
},
created() {
this.updateOrigin();
}
};
</script>

View File

@@ -1,69 +0,0 @@
<!--
* libreevent - textField.vue
*
* Created by Janis Hutz 07/01/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div id="textFields">
<p :style="style">{{ text }}</p>
</div>
</template>
<script>
export default {
name: 'textFieldSeatplanComponent',
props: {
origin: {
type: Number,
'default': 1,
},
text: {
type: String,
'default': 'Untitled',
},
textSize: {
type: Number,
'default': 20,
},
scaleFactor: {
type: Number,
'default': 1,
},
colour: {
type: String,
'default': '#000000',
}
},
data() {
return {
style: 'font-size: 20pt; rotate: 0deg; color: #000000',
};
},
methods: {
updateStyle () {
this.style = `font-size: ${ this.scaleFactor * this.textSize }pt; rotate: ${ 90 * this.origin - 90 }deg; color: ${this.colour}`;
}
},
watch: {
origin () {
this.updateStyle();
},
scaleFactor () {
this.updateStyle();
},
colour () {
this.updateStyle();
},
textSize () {
this.updateStyle();
}
},
created() {
this.updateStyle();
}
};
</script>

View File

@@ -1,180 +0,0 @@
<!--
* libreevent - properties.vue
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div id="circularSeatplan">
<div v-for="row in seats">
<div class="seats" v-for="seat in row" :style="seat.style" :id="seat.id">
<span class="material-symbols-outlined" :style="seat.scaling" @click="selectSeat( seat.row, seat.seat )" v-if="seat.status == 'av'"
:title="seat.displayName + ', Available'">living</span>
<span class="material-symbols-outlined" :style="seat.scaling" v-else-if="seat.status == 'nav'"
:title="seat.displayName + ', Unavailable'">disabled_by_default</span>
<span class="material-symbols-outlined" :style="seat.scaling" v-else-if="seat.status == 'sel'"
:title="seat.displayName + ', Selected'" @click="deselectSeat( seat.row, seat.seat )">check_box</span>
</div>
</div>
</div>
</template>
<style scoped>
.seats {
position: absolute;
}
</style>
<script>
export default {
name: 'circularSeatplanComponent',
props: {
h: {
type: Number,
'default': 100,
},
w: {
type: Number,
'default': 200,
},
scaleFactor: {
type: Number,
'default': 1,
},
origin: {
type: Number,
'default': 1,
},
startingRow: {
type: Number,
'default': 1,
},
data: {
type: Object,
'default': { 'sector': 'A', 'sectorCount': 1, 'unavailableSeats': { 'secAr0s0': 'nav' }, 'categoryInfo': { 'pricing': { '1': { 'displayName': 'Adults - CHF 20.-', 'value': '1', 'price': 20 }, '2': { 'displayName': 'Child (0 - 15.99y) - CHF 15.-', 'value': '2', 'price': 15 } } } }
},
id: {
type: Number,
'default': 1,
}
},
data () {
return {
seats: {},
};
},
methods: {
calculateChairs () {
// Size of seat at scale 1 is 32px
// w & h are normalised
console.log( this.data.seatInfo );
let offsets = {};
if ( this.data.seatInfo ) {
for ( let element in this.data.seatInfo.data[ this.data.sector ] ) {
if ( element < this.data.seatNumbering ) {
for ( let row in this.data.seatInfo.data[ this.data.sector ][ element ] ) {
if ( row !== 'startingRow' ) {
if ( !offsets[ row ] ) offsets[ row ] = 0;
offsets[ row ] += this.data.seatInfo.data[ this.data.sector ][ element ][ row ];
}
}
}
}
}
let w = Math.round( this.w / this.scaleFactor );
let h = Math.round( this.h / this.scaleFactor );
const size = 33;
let count = Math.min( Math.floor( w / size ), Math.floor( h / size ) );
this.seats = {};
for ( let row = this.startingRow; row < count; row++ ) {
let nn = row * ( Math.PI / 2 );
let r = row * size;
this.seats[ row ] = {};
for ( let n = 0; n < nn; n++ ) {
const seatNumber = this.data.numberingDirection === 'right' ? nn - n: n + ( offsets[ row - 1 ] ?? 0 );
this.seats[ row ][ n ] = {
'style': '',
'id': 'comp' + this.id + 'sec' + this.data.sector + 'r' + row + 's' + seatNumber,
'displayName': ( this.data.sectorCount > 1 ? 'Sector ' + this.data.sector + ', ' : '' ) + 'Row ' + row + ', Seat ' + ( seatNumber + 1 ),
'status': 'av',
'row': row,
'seat': n
};
let phi = n * size / ( row * size );
if ( this.origin === 1 ) {
this.seats[ row ][ n ][ 'style' ] = `bottom: ${ r * Math.cos( phi ) * this.scaleFactor }px; left: ${ r * Math.sin( phi ) * this.scaleFactor }px; rotate: ${ phi }rad;`;
} else if ( this.origin === 2 ) {
this.seats[ row ][ n ][ 'style' ] = `bottom: ${ r * Math.cos( phi ) * this.scaleFactor }px; right: ${ r * Math.sin( phi ) * this.scaleFactor }px; rotate: ${ Math.PI * 2 - phi }rad;`;
} else if ( this.origin === 3 ) {
this.seats[ row ][ n ][ 'style' ] = `top: ${ r * Math.cos( phi ) * this.scaleFactor }px; right: ${ r * Math.sin( phi ) * this.scaleFactor }px; rotate: ${ phi + Math.PI }rad;`;
} else if ( this.origin === 4 ) {
this.seats[ row ][ n ][ 'style' ] = `top: ${ r * Math.cos( phi ) * this.scaleFactor }px; left: ${ r * Math.sin( phi ) * this.scaleFactor }px; rotate: ${ Math.PI - phi }rad;`;
}
this.seats[ row ][ n ][ 'scaling' ] = `font-size: ${this.scaleFactor * 200}%; `;
if ( this.data.categoryInfo.color ) {
this.seats[ row ][ n ][ 'style' ] += `color: ${ this.data.categoryInfo.color ?? 'black' };`;
}
if ( this.data.unavailableSeats ) {
if ( this.data.unavailableSeats[ this.seats[ row ][ n ][ 'id' ] ] ) {
this.seats[ row ][ n ][ 'status' ] = this.data.unavailableSeats[ this.seats[ row ][ n ][ 'id' ] ];
}
}
}
}
},
setScaleFactor () {
for ( let row in this.seats ) {
for ( let seat in this.seats[ row ] ) {
this.seats[ row ][ seat ].scaling = `font-size: ${this.scaleFactor * 200}%;`;
}
}
},
selectSeat ( row, seat ) {
console.log( this.data.categoryInfo );
let selectedSeat = this.seats[ row ][ seat ];
selectedSeat[ 'sector' ] = this.data.sector;
selectedSeat[ 'option' ] = this.data.categoryInfo.pricing;
selectedSeat[ 'componentID' ] = this.id;
console.log( selectedSeat );
this.$emit( 'seatSelected', selectedSeat );
},
deselectSeat( row, seat ) {
this.$emit( 'seatDeselected', this.seats[ row ][ seat ] );
this.seats[ row ][ seat ][ 'status' ] = 'av';
},
validateSeatSelection( seatObject, selectedTicketOption ) {
console.log( seatObject );
this.seats[ seatObject[ 'row' ] ][ seatObject[ 'seat' ] ][ 'status' ] = 'sel';
this.seats[ seatObject[ 'row' ] ][ seatObject[ 'seat' ] ][ 'ticketOption' ] = selectedTicketOption;
}
},
watch: {
scaleFactor() {
this.setScaleFactor();
},
h() {
this.calculateChairs();
},
w() {
this.calculateChairs();
},
origin() {
this.calculateChairs();
},
startingRow() {
this.calculateChairs();
},
data () {
this.calculateChairs();
}
},
created() {
this.calculateChairs();
}
};
</script>

View File

@@ -1,175 +0,0 @@
<!--
* libreevent - properties.vue
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div id="rectangularSeatplan">
<div v-for="row in seats" class="rows">
<div class="seats" v-for="seat in row" :style="seat.style" :id="seat.id">
<span class="material-symbols-outlined" :style="seat.scaling" @click="selectSeat( seat.row, seat.seat )" v-if="seat.status == 'av'"
:title="seat.displayName + ', Available'">living</span>
<span class="material-symbols-outlined" :style="seat.scaling" v-else-if="seat.status == 'nav'"
:title="seat.displayName + ', Unavailable'">disabled_by_default</span>
<span class="material-symbols-outlined" :style="seat.scaling" v-else-if="seat.status == 'sel'"
:title="seat.displayName + ', Selected'" @click="deselectSeat( seat.row, seat.seat )">check_box</span>
</div>
</div>
</div>
</template>
<style scoped>
.seats {
position: absolute;
}
</style>
<script>
export default {
name: 'rectangularSeatplanComponent',
props: {
h: {
type: Number,
'default': 100,
},
w: {
type: Number,
'default': 200,
},
scaleFactor: {
type: Number,
'default': 1,
},
startingRow: {
type: Number,
'default': 1,
},
origin: {
type: Number,
'default': 1,
},
data: {
type: Object,
'default': { 'sector': 'A', 'sectorCount': 1, 'unavailableSeats': { 'secAr0s0': 'nav' }, 'categoryInfo': { 'pricing': { '1': { 'displayName': 'Adults - CHF 20.-', 'value': '1', 'price': 20 }, '2': { 'displayName': 'Child (0 - 15.99y) - CHF 15.-', 'value': '2', 'price': 15 } } } }
},
id: {
type: Number,
'default': 1,
},
unavailable: {
type: Object,
'default': {}
}
},
data () {
return {
seats: {},
};
},
methods: {
calculateChairs () {
// Size of seat at scale 1 is 32px
// w & h are normalised
let offsets = {};
if ( this.data.seatInfo ) {
for ( let element in this.data.seatInfo.data[ this.data.sector ] ) {
if ( element < this.data.seatNumbering ) {
for ( let row in this.data.seatInfo.data[ this.data.sector ][ element ] ) {
if ( row !== 'startingRow' ) {
if ( !offsets[ row ] ) offsets[ row ] = 0;
offsets[ row ] += this.data.seatInfo.data[ this.data.sector ][ element ][ row ];
}
}
}
}
}
let w = Math.floor( this.w / this.scaleFactor );
let h = Math.floor( this.h / this.scaleFactor );
const size = 33;
this.seats = {};
for ( let row = 0; row < Math.floor( h / size ); row++ ) {
this.seats[ row ] = {};
for ( let n = 0; n < Math.floor( w / size ); n++ ) {
const seatNumber = this.data.numberingDirection === 'right' ? Math.floor( w / size ) - n: n + ( offsets[ row ] ?? 0 );
this.seats[ row ][ n ] = {
'style': '',
'id': 'comp' + this.id + 'sec' + this.data.sector + 'r' + row + 's' + seatNumber,
'displayName': ( this.data.sectorCount > 1 ? 'Sector ' + this.data.sector + ', ' : '' ) + 'Row ' + ( row + 1 ) + ', Seat ' + ( seatNumber + 1 ),
'status': 'av',
'row': row,
'seat': n
};
if ( this.origin === 1 ) {
this.seats[ row ][ n ][ 'style' ] = `bottom: ${ row * size * this.scaleFactor }px; left: ${ n * size * this.scaleFactor }px; rotate: ${ this.origin / 4 - 0.25 }turn;`;
} else if ( this.origin === 2 ) {
this.seats[ row ][ n ][ 'style' ] = `bottom: ${ row * size * this.scaleFactor }px; right: ${ n * size * this.scaleFactor }px; rotate: ${ this.origin / 4 - 0.25 }turn;`;
} else if ( this.origin === 3 ) {
this.seats[ row ][ n ][ 'style' ] = `top: ${ row * size * this.scaleFactor }px; right: ${ n * size * this.scaleFactor }px; rotate: ${ this.origin / 4 - 0.25 }turn;`;
} else if ( this.origin === 4 ) {
this.seats[ row ][ n ][ 'style' ] = `top: ${ row * size * this.scaleFactor }px; left: ${ n * size * this.scaleFactor }px; rotate: ${ this.origin / 4 - 0.25 }turn;`;
}
this.seats[ row ][ n ][ 'scaling' ] = `font-size: ${this.scaleFactor * 200}%; `;
if ( this.data.categoryInfo.color ) {
this.seats[ row ][ n ][ 'style' ] += `color: ${ this.data.categoryInfo.color ?? 'black' };`;
}
if ( this.unavailable[ this.id ] ) {
if ( this.unavailable[ this.id ][ this.seats[ row ][ n ][ 'id' ] ] ) {
this.seats[ row ][ n ][ 'status' ] = this.unavailable[ this.id ][ this.seats[ row ][ n ][ 'id' ] ];
}
}
}
}
},
setScaleFactor () {
for ( let row in this.seats ) {
for ( let seat in this.seats[ row ] ) {
this.seats[ row ][ seat ].scaling = `font-size: ${this.scaleFactor * 200}%;`;
}
}
},
selectSeat ( row, seat ) {
let selectedSeat = this.seats[ row ][ seat ];
selectedSeat[ 'sector' ] = this.data.sector;
selectedSeat[ 'option' ] = this.data.categoryInfo.pricing;
selectedSeat[ 'componentID' ] = this.id;
this.$emit( 'seatSelected', selectedSeat );
},
deselectSeat( row, seat ) {
this.$emit( 'seatDeselected', this.seats[ row ][ seat ] );
this.seats[ row ][ seat ][ 'status' ] = 'av';
},
validateSeatSelection( seatObject, selectedTicketOption ) {
this.seats[ seatObject[ 'row' ] ][ seatObject[ 'seat' ] ][ 'status' ] = 'sel';
this.seats[ seatObject[ 'row' ] ][ seatObject[ 'seat' ] ][ 'ticketOption' ] = selectedTicketOption;
}
},
watch: {
scaleFactor() {
this.setScaleFactor();
},
h() {
this.calculateChairs();
},
w() {
this.calculateChairs();
},
origin() {
this.calculateChairs();
},
unavailable() {
this.calculateChairs();
}
},
created() {
this.calculateChairs();
}
};
</script>

View File

@@ -1,183 +0,0 @@
<!--
* libreevent - properties.vue
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div id="trapezoidSeatplan">
<div v-for="row in seats">
<div class="seats" v-for="seat in row" :style="seat.style" :id="seat.id">
<span class="material-symbols-outlined" :style="seat.scaling" @click="selectSeat( seat.row, seat.seat )" v-if="seat.status == 'av'"
:title="seat.displayName + ', Available'">living</span>
<span class="material-symbols-outlined" :style="seat.scaling" v-else-if="seat.status == 'nav'"
:title="seat.displayName + ', Unavailable'">disabled_by_default</span>
<span class="material-symbols-outlined" :style="seat.scaling" v-else-if="seat.status == 'sel'"
:title="seat.displayName + ', Selected'" @click="deselectSeat( seat.row, seat.seat )">check_box</span>
</div>
</div>
</div>
</template>
<style scoped>
.seats {
position: absolute;
}
</style>
<script>
export default {
name: 'trapezoidSeatplanComponent',
props: {
h: {
type: Number,
'default': 100,
},
w: {
type: Number,
'default': 200,
},
scaleFactor: {
type: Number,
'default': 1,
},
origin: {
type: Number,
'default': 1,
},
startingRow: {
type: Number,
'default': 1,
},
data: {
type: Object,
'default': { 'sector': 'A', 'sectorCount': 1, 'unavailableSeats': { 'secAr0s0': 'nav' }, 'categoryInfo': { 'pricing': { '1': { 'displayName': 'Adults - CHF 20.-', 'value': '1', 'price': 20 }, '2': { 'displayName': 'Child (0 - 15.99y) - CHF 15.-', 'value': '2', 'price': 15 } } } }
},
id: {
type: Number,
'default': 1,
}
},
data () {
return {
seats: {},
};
},
methods: {
calculateChairs () {
// Size of seat at scale 1 is 32px
// w & h are normalised
let offsets = {};
if ( this.data.seatInfo ) {
for ( let element in this.data.seatInfo.data[ this.data.sector ] ) {
if ( element < this.data.seatNumbering ) {
for ( let row in this.data.seatInfo.data[ this.data.sector ][ element ] ) {
if ( row !== 'startingRow' ) {
if ( !offsets[ row ] ) offsets[ row ] = 0;
offsets[ row ] += this.data.seatInfo.data[ this.data.sector ][ element ][ row ];
}
}
}
}
}
let w = Math.round( this.w / this.scaleFactor );
let h = Math.round( this.h / this.scaleFactor );
const size = 33;
let side = Math.min( w, h ) + 20;
let heightTriangle = Math.floor( Math.sqrt( side ** 2 - ( Math.sqrt( side ** 2 * 2 ) / 2 ) ) );
let sideOffset = size / Math.sqrt( 2 );
let count = Math.floor( heightTriangle / ( sideOffset * 2 ) );
const angle = Math.PI / 4;
this.seats = {};
for ( let row = this.startingRow; row < count; row++ ) {
let nn = 2 + ( row - 1 ) * 2;
this.seats[ row ] = {};
for ( let n = 0; n < nn; n++ ) {
const seatNumber = this.data.numberingDirection === 'right' ? nn - n: n + ( offsets[ row - 1 ] ?? 0 );
this.seats[ row ][ n ] = {
'style': '',
'id': 'comp' + this.id + 'sec' + this.data.sector + 'r' + row + 's' + seatNumber,
'displayName': ( this.data.sectorCount > 1 ? 'Sector ' + this.data.sector + ', ' : '' ) + 'Row ' + row + ', Seat ' + ( seatNumber + 1 ),
'status': 'av',
'row': row,
'seat': n
};
let side = n * sideOffset;
if ( this.origin === 1 ) {
this.seats[ row ][ n ][ 'style' ] = `bottom: ${ ( side + 5 ) * this.scaleFactor }px; left: ${ ( row * sideOffset * 2 - side ) * this.scaleFactor }px; rotate: ${ angle }rad;`;
} else if ( this.origin === 2 ) {
this.seats[ row ][ n ][ 'style' ] = `bottom: ${ ( side + 5 ) * this.scaleFactor }px; right: ${ ( row * sideOffset * 2 - side ) * this.scaleFactor }px; rotate: ${ Math.PI * 2 - angle }rad;`;
} else if ( this.origin === 3 ) {
this.seats[ row ][ n ][ 'style' ] = `top: ${ ( side + 5 ) * this.scaleFactor }px; right: ${ ( row * sideOffset * 2 - side ) * this.scaleFactor }px; rotate: ${ angle + Math.PI }rad;`;
} else if ( this.origin === 4 ) {
this.seats[ row ][ n ][ 'style' ] = `top: ${ ( side + 5 ) * this.scaleFactor }px; left: ${ ( row * sideOffset * 2 - side ) * this.scaleFactor }px; rotate: ${ Math.PI - angle }rad;`;
}
this.seats[ row ][ n ][ 'scaling' ] = `font-size: ${this.scaleFactor * 200}%; `;
if ( this.data.categoryInfo.color ) {
this.seats[ row ][ n ][ 'style' ] += `color: ${ this.data.categoryInfo.color ?? 'black' };`;
}
if ( this.data.unavailableSeats ) {
if ( this.data.unavailableSeats[ this.seats[ row ][ n ][ 'id' ] ] ) {
this.seats[ row ][ n ][ 'status' ] = this.data.unavailableSeats[ this.seats[ row ][ n ][ 'id' ] ];
}
}
}
}
},
setScaleFactor () {
for ( let row in this.seats ) {
for ( let seat in this.seats[ row ] ) {
this.seats[ row ][ seat ].scaling = `font-size: ${this.scaleFactor * 200}%;`;
}
}
},
selectSeat ( row, seat ) {
let selectedSeat = this.seats[ row ][ seat ];
selectedSeat[ 'sector' ] = this.data.sector;
selectedSeat[ 'option' ] = this.data.categoryInfo.pricing;
selectedSeat[ 'componentID' ] = this.id;
this.$emit( 'seatSelected', selectedSeat );
},
deselectSeat( row, seat ) {
this.$emit( 'seatDeselected', this.seats[ row ][ seat ] );
this.seats[ row ][ seat ][ 'status' ] = 'av';
},
validateSeatSelection( seatObject, selectedTicketOption ) {
console.log( seatObject );
this.seats[ seatObject[ 'row' ] ][ seatObject[ 'seat' ] ][ 'status' ] = 'sel';
this.seats[ seatObject[ 'row' ] ][ seatObject[ 'seat' ] ][ 'ticketOption' ] = selectedTicketOption;
}
},
watch: {
scaleFactor() {
this.setScaleFactor();
},
h() {
this.calculateChairs();
},
w() {
this.calculateChairs();
},
origin() {
this.calculateChairs();
},
startingRow() {
this.calculateChairs();
},
data () {
console.log( 'data changed' );
this.calculateChairs();
}
},
created() {
this.calculateChairs();
}
};
</script>

View File

@@ -1,114 +0,0 @@
<!--
* libreevent - stages.vue
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div id="stages" class="stages">
<div id="rectangular" v-if="shape == 'rectangular'" class="stages" :style="style"></div>
<div id="trapezoid" v-else-if="shape == 'trapezoid'" class="stages"><div id="trapezoid-ingredient" :style="trapezoidStyle"><div id="trapezoid-line"></div></div></div>
<div id="circular" v-else-if="shape == 'circular'" class="stages"><div id="circular-ingredient" :style="circularStyle"></div></div>
</div>
</template>
<style scoped>
.stages {
height: 100%;
width: 100%;
}
#rectangular {
border-color: black;
border-width: 2px;
background-color: var( --popup-color );
}
#circular, #trapezoid {
overflow: hidden;
}
#trapezoid-ingredient {
display: flex;
justify-content: center;
align-items: flex-end;
height: 200%;
width: 200%;
position: relative;
bottom: 50%;
right: 50%;
}
#trapezoid-line {
border: black solid 1px;
height: 50%;
width: 100%;
display: block;
background-color: var( --popup-color );
}
#circular-ingredient {
border: solid black 2px;
border-radius: 100%;
height: 199%;
width: 199%;
position: relative;
background-color: var( --popup-color );
}
</style>
<script>
export default {
name: 'stagesSeatplanComponent',
props: {
origin: {
type: Number,
'default': 1,
},
shape: {
type: String,
'default': 'rectangular',
},
},
data() {
return {
style: 'border-style: none none solid none',
circularStyle: 'top: 0; left 100%;',
trapezoidStyle: 'rotate: 45deg',
};
},
methods: {
updateOrigin () {
if ( this.origin === 1 ) {
this.style = 'border-style: none none solid none';
this.circularStyle = 'top: 0; right: 100%;';
this.trapezoidStyle = 'rotate: 45deg';
} else if ( this.origin === 2 ) {
this.style = 'border-style: none solid none none';
this.circularStyle = 'top: 0; right: 0;';
this.trapezoidStyle = 'rotate: 135deg';
} else if ( this.origin === 3 ) {
this.style = 'border-style: solid none none none';
this.circularStyle = 'top: -100%; right: 0;';
this.trapezoidStyle = 'rotate: 225deg';
} else if ( this.origin === 4 ) {
this.style = 'border-style: none none none solid';
this.circularStyle = 'top: -100%; right: 100%;';
this.trapezoidStyle = 'rotate: 315deg';
}
}
},
watch: {
origin () {
this.updateOrigin();
}
},
created() {
this.updateOrigin();
}
};
</script>

View File

@@ -1,120 +0,0 @@
<!--
* libreevent - standing.vue
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div id="stages" class="stages">
<div id="rectangular" v-if="shape == 'rectangular'" class="stages" :style="style"></div>
<div id="trapezoid" v-else-if="shape == 'trapezoid'" class="stages"><div id="trapezoid-ingredient" :style="trapezoidStyle"><div id="trapezoid-line"></div></div></div>
<div id="circular" v-else-if="shape == 'circular'" class="stages"><div id="circular-ingredient" :style="circularStyle"></div></div>
</div>
</template>
<style scoped>
.stages {
height: 100%;
width: 100%;
}
#rectangular {
border-color: black;
border-width: 2px;
}
#circular, #trapezoid {
overflow: hidden;
}
#trapezoid-ingredient {
display: flex;
justify-content: center;
align-items: flex-end;
height: 200%;
width: 200%;
position: relative;
bottom: 50%;
right: 50%;
}
#trapezoid-line {
border: black solid 1px;
height: 50%;
width: 100%;
display: block;
background-color: var( --popup-color );
}
#circular-ingredient {
border: solid black 2px;
border-radius: 100%;
height: 199%;
width: 199%;
position: relative;
background-color: var( --popup-color );
}
</style>
<script>
export default {
name: 'stagesSeatplanComponent',
props: {
origin: {
type: Number,
'default': 1,
},
shape: {
type: String,
'default': 'rectangular',
},
color: {
type: Object,
'default': { 'fg': '#FFFFFF' }
}
},
data() {
return {
style: 'border-style: none none solid none;',
circularStyle: 'top: 0; left 100%;',
trapezoidStyle: 'rotate: 45deg',
};
},
methods: {
updateOrigin () {
if ( this.origin === 1 ) {
this.style = 'border-style: none none solid none;';
this.circularStyle = 'top: 0; right: 100%;';
this.trapezoidStyle = 'rotate: 45deg';
} else if ( this.origin === 2 ) {
this.style = 'border-style: none solid none none;';
this.circularStyle = 'top: 0; right: 0;';
this.trapezoidStyle = 'rotate: 135deg';
} else if ( this.origin === 3 ) {
this.style = 'border-style: solid none none none;';
this.circularStyle = 'top: -100%; right: 0;';
this.trapezoidStyle = 'rotate: 225deg';
} else if ( this.origin === 4 ) {
this.style = 'border-style: none none none solid;';
this.circularStyle = 'top: -100%; right: 100%;';
this.trapezoidStyle = 'rotate: 315deg';
}
this.style += ` background-color: ${this.color}; color: ${this.color}`;
this.circularStyle += ` background-color: ${this.color}; color: ${this.color}`;
this.trapezoidStyle += ` background-color: ${this.color}; color: ${this.color}`;
},
},
watch: {
origin () {
this.updateOrigin();
}
},
created() {
this.updateOrigin();
}
};
</script>

View File

@@ -1,69 +0,0 @@
<!--
* libreevent - textField.vue
*
* Created by Janis Hutz 07/01/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div id="textFields">
<p :style="style">{{ text }}</p>
</div>
</template>
<script>
export default {
name: 'textFieldSeatplanComponent',
props: {
origin: {
type: Number,
'default': 1,
},
text: {
type: String,
'default': 'Untitled',
},
textSize: {
type: Number,
'default': 20,
},
scaleFactor: {
type: Number,
'default': 1,
},
colour: {
type: String,
'default': '#000000',
}
},
data() {
return {
style: 'font-size: 20pt; rotate: 0deg; color: #000000',
};
},
methods: {
updateStyle () {
this.style = `font-size: ${ this.scaleFactor * this.textSize }pt; rotate: ${ 90 * this.origin - 90 }deg; color: ${this.colour}`;
}
},
watch: {
origin () {
this.updateStyle();
},
scaleFactor () {
this.updateStyle();
},
colour () {
this.updateStyle();
},
textSize () {
this.updateStyle();
}
},
created() {
this.updateStyle();
}
};
</script>

View File

@@ -1,583 +0,0 @@
<!-- eslint-disable no-undef -->
<!--
* libreevent - window.vue
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div id="window">
<div class="parent" id="parent" @wheel="( e ) => { handleScroll( e ); }" @mousemove="( e ) => { handleDrag( e ); }" @mousedown="( e ) => { setOffset( e ); }">
<div class="content-parent">
<Vue3DraggableResizable v-for="draggable in draggables" :initW="draggable.w" :initH="draggable.h" :x="draggable.x" :y="draggable.y" :w="draggable.w" :h="draggable.h"
:active="false" :draggable="false" :resizable="false" :parent="true" class="draggable-box">
<circularSeatplanComponent v-if="draggable.shape == 'circular' && draggable.type == 'seat'" :ref="'component' + draggable.id"
:scale-factor="scaleFactor" :w="draggable.w" :h="draggable.h" :origin="draggable.origin" :starting-row="draggable.startingRow"
:data="draggable.data" :id="draggable.id" :unavailable="unavailableSeats"
@seatSelected="( seat ) => { seatSelected( seat ) }" @seatDeselected="( seat ) => { seatDeselected( seat ) }"></circularSeatplanComponent>
<trapezoidSeatplanComponent v-else-if="draggable.shape == 'trapezoid' && draggable.type == 'seat'" :ref="'component' + draggable.id"
:scale-factor="scaleFactor" :w="draggable.w" :h="draggable.h" :origin="draggable.origin" :starting-row="draggable.startingRow"
:data="draggable.data" :id="draggable.id" :unavailable="unavailableSeats"
@seatSelected="( seat ) => { seatSelected( seat ) }" @seatDeselected="( seat ) => { seatDeselected( seat ) }"></trapezoidSeatplanComponent>
<rectangularSeatplanComponent v-else-if="draggable.shape == 'rectangular' && draggable.type == 'seat'" :ref="'component' + draggable.id"
:scale-factor="scaleFactor" :w="draggable.w" :h="draggable.h" :origin="draggable.origin" :starting-row="draggable.startingRow"
:data="draggable.data" :id="draggable.id" :unavailable="unavailableSeats"
@seatSelected="( seat ) => { seatSelected( seat ) }" @seatDeselected="( seat ) => { seatDeselected( seat ) }"></rectangularSeatplanComponent>
<stagesSeatplanComponent :ref="'component' + draggable.id" v-else-if="draggable.type == 'stage'" :origin="draggable.origin" :shape="draggable.shape"></stagesSeatplanComponent>
<standingSeatplanComponent :ref="'component' + draggable.id" v-else-if="draggable.type == 'stand'" :origin="draggable.origin"
:shape="draggable.shape" @click="standing( draggable.id )" :color="draggable.data.categoryInfo.color"></standingSeatplanComponent>
<textFieldSeatplanComponent :ref="'component' + draggable.id" v-else-if="draggable.type == 'text'" :text="draggable.text.text" :text-size="draggable.text.textSize"
:colour="draggable.text.colour" :origin="draggable.origin" :scale-factor="scaleFactor"></textFieldSeatplanComponent>
</Vue3DraggableResizable>
</div>
</div>
<div class="toolbar">
<button title="Zoom in [+]" @click="zoom( 0.2 )"><span class="material-symbols-outlined">zoom_in</span></button>
<button title="Reset zoom [=]" @click="zoom( 1 );"><span class="material-symbols-outlined">center_focus_strong</span></button>
<button title="Zoom out [-]" @click="zoom( -0.2 )"><span class="material-symbols-outlined">zoom_out</span></button>
</div>
<sideCartView :cart="cart" :name="event.name" ref="cart" type="true"></sideCartView>
<notifications ref="notification" location="topright"></notifications>
<popups ref="popups" size="normal" @data="data => { reserveTicket( data ) }"
@ticket="data => { standingTicketHandling( data ) }"></popups>
</div>
</template>
<script>
import Vue3DraggableResizable from 'vue3-draggable-resizable';
import circularSeatplanComponent from '@/components/seatplan/userApp/seatplanComponents/seats/circular.vue';
import rectangularSeatplanComponent from '@/components/seatplan/userApp/seatplanComponents/seats/rectangular.vue';
import trapezoidSeatplanComponent from '@/components/seatplan/userApp/seatplanComponents/seats/trapezoid.vue';
import stagesSeatplanComponent from '@/components/seatplan/userApp/seatplanComponents/stages.vue';
import standingSeatplanComponent from '@/components/seatplan/userApp/seatplanComponents/standing.vue';
import textFieldSeatplanComponent from '@/components/seatplan/userApp/seatplanComponents/textField.vue';
import notifications from '@/components/notifications/notifications.vue';
import popups from '@/components/notifications/popups.vue';
import 'vue3-draggable-resizable/dist/Vue3DraggableResizable.css';
import sideCartView from '@/components/sideCartView.vue';
export default {
'name': 'window',
components: {
Vue3DraggableResizable,
circularSeatplanComponent,
rectangularSeatplanComponent,
trapezoidSeatplanComponent,
stagesSeatplanComponent,
standingSeatplanComponent,
textFieldSeatplanComponent,
notifications,
popups,
sideCartView,
},
data() {
return {
draggables: { 1: { 'x': 100, 'y': 100, 'h': 100, 'w': 250, 'active': false, 'draggable': true, 'resizable': true, 'id': 1, 'origin': 1, 'shape': 'rectangular', 'type': 'seat', 'startingRow': 1, 'seatCountingStartingPoint': 1, 'sector': 'A', 'text': { 'text': 'TestText', 'textSize': 20, 'colour': '#20FFFF' }, 'ticketCount': 1, 'category': 1 } },
event: { 'name': 'TestEvent2', 'location': 'TestLocation2', 'eventID': 'test2', 'date': '2023-07-15', 'currency': 'CHF', 'categories': { '1': { 'price': { '1': 25, '2': 35 }, 'bg': 'black', 'fg': 'white', 'name': 'Category 1' }, '2': { 'price': { '1': 15, '2': 20 }, 'bg': 'green', 'fg': 'white', 'name': 'Category 2' } }, 'ageGroups': { '1': { 'id': 1, 'name': 'Child', 'age': '0 - 15.99' }, '2': { 'id': 2, 'name': 'Adult' } }, 'maxTickets': 2 },
available: { 'redo': false, 'undo': false },
scaleFactor: 1,
sizePoll: null,
prevSize: { 'h': window.innerHeight, 'w': window.innerWidth },
zoomFactor: 1,
standardDeviation: { 'currentTop': 0, 'currentLeft': 0 },
movePos: { 'top': 0, 'left': 0, 'isMoving': false, 'isSet': false },
generalSettings: { 'namingScheme': 'numeric' },
selectedSeat: {},
cart: {},
unavailableSeats: {},
};
},
methods: {
/*
Coords are from top left corner of box.
The below function is executed as the init hook (created hook)
of vue.js, so whenever this particular page is loaded.
It loads seat plan data and starts the event listeners
for keyevents (like +, -, =)
*/
runHook () {
let self = this;
this.zoomFactor = sessionStorage.getItem( 'zoom' ) ? parseFloat( sessionStorage.getItem( 'zoom' ) ) : 1;
document.onkeydown = function ( event ) {
if ( event.key === '+' ) {
self.zoom( 0.2 );
} else if ( event.key === '-' ) {
self.zoom( -0.2 );
} else if ( event.key === '=' ) {
self.zoom( 1 );
}
};
this.seatPlanInit();
},
seatPlanInit () {
// Load cart
this.cart = localStorage.getItem( 'cart' ) ? JSON.parse( localStorage.getItem( 'cart' ) ): {};
// Load seatplan from server
let height = $( document ).height() * 0.8;
this.scaleFactor = ( height / 900 ) * this.zoomFactor;
fetch( '/getAPI/getEvent?event=' + sessionStorage.getItem( 'selectedTicket' ) ).then( res => {
if ( res.status === 200 ) {
res.json().then( json => {
this.event = json ?? {};
fetch( localStorage.getItem( 'url' ) + '/getAPI/getSeatplan?location=' + this.event.location ).then( res => {
if ( res.status === 200 ) {
res.json().then( data => {
this.draggables = this.scaleUp( data.data );
this.prepSeatplan( data.seatInfo );
} );
} else if ( res.status === 500 ) {
if ( sessionStorage.getItem( 'seatplan' ) ) {
this.draggables = this.scaleUp( JSON.parse( sessionStorage.getItem( 'seatplan' ) ) );
this.prepSeatplan( {} );
}
}
} );
} );
}
} );
},
prepSeatplan ( seatInfo ) {
// Mark all selected seats + all unavailable seats
let categoryDetails = {};
for ( let category in this.event.categories ) {
categoryDetails[ category ] = {};
for ( let group in this.event.ageGroups ) {
categoryDetails[ category ][ group ] = {};
categoryDetails[ category ][ group ] = { 'displayName': this.event.ageGroups[ group ].name + ( this.event.ageGroups[ group ].age ? ' (' + this.event.ageGroups[ group ].age + ')' : '' ) + ' - ' + this.event.currency + ' ' + this.event.categories[ category ].price[ group ], 'value': group, 'price': this.event.categories[ category ].price[ group ] };
}
}
for ( let element in this.draggables ) {
this.draggables[ element ][ 'data' ] = {
'sector': this.draggables[ element ][ 'sector' ],
'categoryInfo': {
'pricing': categoryDetails[ this.draggables[ element ][ 'category' ] ],
'color': this.event.categories[ this.draggables[ element ][ 'category' ] ][ 'fg' ]
},
'seatInfo': seatInfo,
'seatNumbering': this.draggables[ element ].seatNumberingPosition,
'numberingDirection': this.draggables[ element ].numberingDirection,
};
}
this.seatChecks();
// TODO: FUTURE Trim scroll box to about 200px more than seatplan size
sessionStorage.setItem( 'seatplan', JSON.stringify( this.scaleDown( this.draggables ) ) );
window.addEventListener( 'visibilitychange', () => {
this.seatPlanInit();
}, 1 );
},
seatChecks () {
let self = this;
let allSeatsAvailable = true;
fetch( localStorage.getItem( 'url' ) + '/getAPI/getReservedSeats?event=' + this.event.eventID ).then( res => {
if ( res.status === 200 ) {
let unavailableSeats = {};
res.json().then( data => {
for ( let seat in data.reserved ) {
if ( data.reserved[ seat ] ) {
if ( !unavailableSeats[ data.reserved[ seat ].component ] ) {
unavailableSeats[ data.reserved[ seat ].component ] = {};
}
unavailableSeats[ data.reserved[ seat ].component ][ data.reserved[ seat ].id ] = 'nav';
}
}
for ( let seat in data.user ) {
if ( data.user[ seat ] ) {
if ( !unavailableSeats[ data.user[ seat ].component ] ) {
unavailableSeats[ data.user[ seat ].component ] = {};
}
unavailableSeats[ data.user[ seat ].component ][ data.user[ seat ].id ] = 'sel';
}
}
let tickets = {};
if ( this.cart[ this.event.eventID ] ) {
tickets = this.cart[ this.event.eventID ][ 'tickets' ];
}
if ( data.user ) {
for ( let element in tickets ) {
if ( !data.user[ element ] ) {
allSeatsAvailable = false;
if ( Object.keys( this.cart[ this.event.eventID ][ 'tickets' ] ).length > 1 ) {
delete this.cart[ this.event.eventID ][ 'tickets' ][ element ];
} else {
delete this.cart[ this.event.eventID ];
}
}
}
} else {
delete this.cart[ this.event.eventID ];
allSeatsAvailable = false;
}
this.unavailableSeats = unavailableSeats;
if ( !allSeatsAvailable ) {
setTimeout( () => {
self.$refs.popups.openPopup( 'We are sorry to tell you that since the last time the seat plan was refreshed, one or more of the seats you have selected has/have been taken.', {}, 'string' );
}, 500 );
localStorage.setItem( 'cart', JSON.stringify( this.cart ) );
}
} );
} else {
console.error( 'unable to load' );
}
} );
},
eventHandler () {
if ( this.prevSize.h != window.innerHeight || this.prevSize.w != window.innerWidth ) {
this.prevSize = { 'h': window.innerHeight, 'w': window.innerWidth };
this.loadSeatplan();
}
},
handleScroll ( e ) {
e.preventDefault();
if ( e.deltaY > 0 ) {
this.zoom( 0.2 );
} else {
this.zoom( -0.2 );
}
},
setOffset( e ) {
this.standardDeviation.currentLeft = e.clientX;
this.standardDeviation.currentTop = e.clientY;
},
handleDrag ( e ) {
if ( e.buttons === 1 ) {
let parent = document.getElementById( 'parent' );
if ( !this.movePos.isSet ) {
this.movePos.left = parent.scrollWidth - parent.scrollLeft;
this.movePos.top = parent.scrollHeight - parent.scrollTop;
this.movePos.isSet = true;
}
this.movePos.isMoving = true;
e.preventDefault();
let valueTop = parent.scrollHeight - ( e.clientY - this.standardDeviation.currentTop + this.movePos.top );
let valueLeft = parent.scrollWidth - ( e.clientX - this.standardDeviation.currentLeft + this.movePos.left );
parent.scrollTop = valueTop > 0 ? valueTop : 0;
parent.scrollLeft = valueLeft > 0 ? valueLeft : 0;
} else {
if ( this.movePos.isMoving ) {
let parent = document.getElementById( 'parent' );
this.movePos.left = parent.scrollWidth - parent.scrollLeft;
this.movePos.top = parent.scrollHeight - parent.scrollTop;
this.movePos.isMoving = false;
}
}
},
scaleDown ( valueArray ) {
const allowedAttributes = [ 'w', 'h', 'x', 'y' ];
let returnArray = {};
for ( let entry in valueArray ) {
returnArray[ entry ] = {};
for ( let attributes in valueArray[ entry ] ) {
if ( allowedAttributes.includes( attributes ) ) {
returnArray[ entry ][ attributes ] = Math.round( ( valueArray[ entry ][ attributes ] / this.scaleFactor ) * 1000 ) / 1000;
} else {
returnArray[ entry ][ attributes ] = valueArray[ entry ][ attributes ];
}
}
}
return returnArray;
},
loadSeatplan () {
/*
Calculate scale factor (this adds support for differently sized screens)
900px is the "default" height
*/
let height = $( document ).height() * 0.8;
this.scaleFactor = ( height / 900 ) * this.zoomFactor;
/*
Load seatplan
*/
if ( sessionStorage.getItem( 'seatplan' ) ) {
this.draggables = this.scaleUp( JSON.parse( sessionStorage.getItem( 'seatplan' ) ) );
}
},
scaleUp ( valueArray ) {
const allowedAttributes = [ 'w', 'h', 'x', 'y' ];
let returnArray = {};
for ( let entry in valueArray ) {
returnArray[ entry ] = {};
for ( let attributes in valueArray[ entry ] ) {
if ( allowedAttributes.includes( attributes ) ) {
returnArray[ entry ][ attributes ] = Math.round( ( valueArray[ entry ][ attributes ] * this.scaleFactor ) * 1000 ) / 1000;
} else {
returnArray[ entry ][ attributes ] = valueArray[ entry ][ attributes ];
}
}
}
return returnArray;
},
zoom ( scale ) {
if ( scale == 1 ) {
this.zoomFactor = 1;
sessionStorage.setItem( 'zoom', this.zoomFactor );
this.loadSeatplan();
} else {
if ( ( this.zoomFactor < 0.3 && scale < 0 ) || ( this.zoomFactor > 2.9 && scale > 0 ) ) {
console.log( 'maxZoomLevelReached' );
} else {
this.zoomFactor += scale;
}
sessionStorage.setItem( 'zoom', this.zoomFactor );
this.loadSeatplan();
}
},
seatSelected ( seat ) {
this.selectedSeat = seat;
if ( Object.keys( seat.option ).length > 1 ) {
this.$refs.popups.openPopup( 'Please choose a ticket option', seat.option, 'selection', 'adult' );
} else {
this.reserveTicket( { 'status': 'ok', 'data': Object.keys( seat.option )[ 0 ][ 'value' ], 'id': this.selectedSeat.componentID } );
}
},
cartHandling ( operation, data ) {
if ( operation === 'select' ) {
if ( this.cart[ this.event.eventID ] ) {
this.cart[ this.event.eventID ][ 'tickets' ][ this.selectedSeat.id ] = { 'displayName': this.selectedSeat.displayName, 'price': this.selectedSeat.option[ data ].price, 'id': this.selectedSeat.id, 'option': data, 'comp': this.selectedSeat.componentID };
} else {
this.cart[ this.event.eventID ] = { 'displayName': this.event.name, 'tickets': {}, 'eventID': this.event.eventID };
this.cart[ this.event.eventID ][ 'tickets' ][ this.selectedSeat.id ] = { 'displayName': this.selectedSeat.displayName, 'price': this.selectedSeat.option[ data ].price, 'id': this.selectedSeat.id, 'option': data, 'comp': this.selectedSeat.componentID };
}
} else if ( operation === 'deselect' ) {
if ( Object.keys( this.cart[ this.event.eventID ][ 'tickets' ] ).length > 1 ) {
delete this.cart[ this.event.eventID ][ 'tickets' ][ this.selectedSeat.id ];
} else {
delete this.cart[ this.event.eventID ];
}
}
this.$refs.cart.calculateTotal();
localStorage.setItem( 'cart', JSON.stringify( this.cart ) );
},
reserveTicket ( option ) {
if ( option.status == 'ok' && option.data ) {
// Make call to server to reserve ticket to have server also keep track of reserved tickets
let progressNot = this.$refs.notification.createNotification( 'Reserving ticket...', 20, 'progress', 'normal' );
const options = {
method: 'post',
body: JSON.stringify( { 'id': this.selectedSeat[ 'id' ], 'component': this.selectedSeat[ 'componentID' ], 'ticketOption': option.data, 'eventID': this.event.eventID, 'category': this.draggables[ this.selectedSeat[ 'componentID' ] ].category, 'name': this.selectedSeat.displayName } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/API/reserveTicket', options ).then( res => {
if ( res.status === 200 ) {
this.$refs.notification.cancelNotification( progressNot );
this.$refs[ 'component' + this.selectedSeat.componentID ][ 0 ].validateSeatSelection( this.selectedSeat, option.data );
this.cartHandling( 'select', option.data );
} else if ( res.status === 409 ) {
setTimeout( () => {
this.$refs.popups.openPopup( 'Unfortunately, the seat you just tried to select was reserved by somebody else since the last time the seat plan was refreshed. Please select another one. We are sorry for the inconvenience.', {}, 'string' );
}, 300 );
} else if ( res.status === 418 ) {
setTimeout( () => {
this.$refs.popups.openPopup( 'We are sorry, but you have already selected the maximum amount of tickets you can buy at once.', {}, 'string' );
}, 300 );
}
} );
}
},
seatDeselected ( seat ) {
this.selectedSeat = seat;
this.cartHandling( 'deselect' );
// Make call to server to deselect ticket
const options = {
method: 'post',
body: JSON.stringify( { 'id': seat[ 'id' ], 'eventID': this.event.eventID, 'component': seat.componentID } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/API/deselectTicket', options );
},
standing ( id ) {
const d = this.draggables[ id ];
const evG = this.event.ageGroups;
let count = {};
if ( this.cart[ this.event.eventID ] ) {
for ( let ageGroup in evG ) {
if ( this.cart[ this.event.eventID ][ 'tickets' ][ 'ticket' + id + '_' + ageGroup ] ) {
count[ ageGroup ] = this.cart[ this.event.eventID ][ 'tickets' ][ 'ticket' + id + '_' + ageGroup ].count;
} else {
count[ ageGroup ] = 0;
}
}
} else {
for ( let ageGroup in evG ) {
count[ ageGroup ] = 0;
}
}
this.$refs.popups.openPopup( 'Select tickets', {
'id': id,
'max': d.ticketCount,
'name': 'Sector ' + d.sector + ' Ticket ' + id,
'ageGroups': this.event.ageGroups,
'currency': this.event.currency,
'price': this.event.categories[ d.category ],
'count': count
}, 'tickets' );
},
standingTicketHandling ( data ) {
if ( !this.cart[ this.event.eventID ] ) {
this.cart[ this.event.eventID ] = { 'displayName': this.event.name, 'tickets': {}, 'eventID': this.event.eventID };
}
let groups = Object.keys( data.data );
let group, ready = true;
let postInterval = setInterval( () => {
if ( ready ) {
ready = false;
if ( groups.length > 0 ) {
group = groups.pop();
if ( data.data[ group ] > 0 ) {
const options = {
method: 'post',
body: JSON.stringify( { 'id': 'ticket' + data.component + '_' + group, 'component': data.component, 'ticketOption': group, 'eventID': this.event.eventID, 'count': data.data[ group ], 'category': this.draggables[ data.component ].category, 'name': 'Ticket ' + data.component + ' (' + this.event.ageGroups[ group ].name + ')' } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/API/reserveTicket', options ).then( res => {
ready = true;
if ( res.status === 200 ) {
this.cart[ this.event.eventID ][ 'tickets' ][ 'ticket' + data.component + '_' + group ] = { 'displayName': 'Ticket ' + data.component + ' (' + this.event.ageGroups[ group ].name + ')', 'price': this.event.categories[ this.draggables[ data.component ].category ].price[ group ], 'id': 'ticket' + data.component + '_' + group, 'count': data.data[ group ], 'comp': data.component };
} else if ( res.status === 409 ) {
res.json().then( dat => {
if ( dat.count < 1 ) {
if ( Object.keys( this.cart[ this.event.eventID ][ 'tickets' ] ).length <= 1 ) {
try {
delete this.cart[ this.event.eventID ];
} catch {
console.log( 'element nonexistent' );
}
} else {
delete this.cart[ this.event.eventID ][ 'tickets' ][ 'ticket' + data.component + '_' + group ];
}
}
this.cart[ this.event.eventID ][ 'tickets' ][ 'ticket' + data.component + '_' + group ] = { 'displayName': 'Ticket ' + data.component + ' (' + this.event.ageGroups[ group ].name + ')', 'price': this.event.categories[ this.draggables[ data.component ].category ].price[ group ], 'id': 'ticket' + data.component + '_' + group, 'count': dat.count, 'comp': data.component };
} );
setTimeout( () => {
this.$refs.popups.openPopup( 'Unfortunately, you have selected more tickets than were still available. The maximum amount of tickets that are available have been selected for you automatically. We are sorry for the inconvenience.', {}, 'string' );
}, 300 );
} else if ( res.status === 418 ) {
setTimeout( () => {
this.$refs.popups.openPopup( 'We are sorry, but you have already selected the maximum amount of tickets you can buy at once.', {}, 'string' );
}, 300 );
}
if ( Object.keys( this.cart[ this.event.eventID ][ 'tickets' ] ).length < 1 ) {
delete this.cart[ this.event.eventID ];
}
this.$refs.cart.calculateTotal();
localStorage.setItem( 'cart', JSON.stringify( this.cart ) );
} );
} else {
if ( this.cart[ this.event.eventID ] ) {
if ( this.cart[ this.event.eventID ][ 'tickets' ][ 'ticket' + data.component + '_' + group ] ) {
delete this.cart[ this.event.eventID ][ 'tickets' ][ 'ticket' + data.component + '_' + group ];
if ( this.cart[ this.event.eventID ] ) {
if ( Object.keys( this.cart[ this.event.eventID ][ 'tickets' ] ).length < 1 ) {
delete this.cart[ this.event.eventID ];
}
}
const options = {
method: 'post',
body: JSON.stringify( { 'id': 'ticket' + data.component + '_' + group, 'eventID': this.event.eventID, 'component': data.component } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/API/deselectTicket', options ).then( res => {
ready = true;
} );
localStorage.setItem( 'cart', JSON.stringify( this.cart ) );
} else {
ready = true;
}
} else {
ready = true;
}
}
} else {
clearInterval( postInterval );
}
}
}, 250 );
}
},
created () {
this.runHook();
this.sizePoll = setInterval( this.eventHandler, 250 );
},
unmounted() {
clearInterval( this.sizePoll );
},
};
</script>
<style scoped>
.parent {
height: 80vh;
width: 90vw;
top: 90px;
left: 5vw;
position: absolute;
border: black 1px solid;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
overflow: scroll;
}
.draggable-box {
cursor: default;
}
.content-parent {
width: 400vw;
height: 400vw;
}
.toolbar {
display: flex;
position: absolute;
top: 90px;
left: 5.5vw;
}
.toolbar button {
margin-top: 10%;
cursor: pointer;
}
.toolbar button:disabled {
cursor: default;
}
@media only screen and (min-width: 999px) {
.parent {
width: 70vw;
}
}
</style>

View File

@@ -1,96 +0,0 @@
<!-- eslint-disable no-undef -->
<!--
* libreevent - rightClickMenu.vue
*
* Created by Janis Hutz 07/02/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<div class="right-click-menu" id="right-click-menu">
<ul>
<li v-for="option in options" @click="executeCommand( option.command )"><span class="material-symbols-outlined right-click-symbols">{{ option.symbol }}</span>{{ option.display }}</li>
</ul>
</div>
<div class="right-click-menu-disable" @click="closeRightClickMenu()" id="right-click-menu-disable"></div>
</div>
</template>
<script>
export default {
name: 'rightClickMenu',
data() {
return {
options: {},
};
},
methods: {
openRightClickMenu( event, options ) {
$( '#right-click-menu' ).show( 100 );
// Get cursor position
$( '#right-click-menu' ).css( 'top', event.clientY + 'px' );
$( '#right-click-menu' ).css( 'left', event.clientX + 'px' );
this.options = options;
$( '#right-click-menu-disable' ).show();
},
closeRightClickMenu() {
$( '#right-click-menu' ).hide( 100 );
$( '#right-click-menu-disable' ).hide();
},
executeCommand ( command ) {
this.closeRightClickMenu();
this.$emit( 'command', command );
}
}
};
</script>
<style scoped>
/* Right click menu */
.right-click-symbols {
margin-right: 2%;
font-size: 100%;
}
.right-click-menu {
text-align: justify;
display: none;
z-index: 10;
width: 10vw;
position: fixed;
background-color: var( --overlay-color );
color: var( --secondary-color );
padding: 0.5%;
}
.right-click-menu ul {
list-style: none;
padding: 0;
margin: 0;
}
.right-click-menu ul li {
cursor: pointer;
width: 90%;
padding: 5% 5%;
}
.right-click-menu ul li:hover {
background-color: var( --hover-color );
}
.right-click-menu-disable {
position: fixed;
display: none;
opacity: 0;
height: 100vh;
width: 100vw;
top: 0;
left: 0;
}
</style>

View File

@@ -1,221 +0,0 @@
<!-- eslint-disable no-undef -->
<!--
* libreevent - settings.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="settings-wrapper">
<table class="settings-toggles">
<tr class="settings-option" v-for="setting in settings">
<td class="info-wrapper">
{{ setting.display }}
<div class="info-container" @mouseenter="showInfo( setting.id )" @mouseleave="hideInfo( setting.id )">
<span class="material-symbols-outlined info-icon">info</span>
<div class="info-box" :id="setting.id">
<div class="info-box-container" v-html="setting.tooltip">
</div>
</div>
</div>
</td>
<td v-if="setting.type == 'toggle'">
<label class="switch">
<input type="checkbox" v-model="setting.value">
<span class="slider round"></span>
</label>
</td>
<td v-else-if="setting.type == 'select'">
<select v-model="setting.value">
<option v-for="option in setting.restrictions" :value="option.value">{{ option.displayName }}</option>
</select>
</td>
<td v-else-if="setting.type == 'number'">
<input type="number" v-model="setting.value" :min="setting.restrictions.min" :max="setting.restrictions.max">
</td>
<td v-else-if="setting.type == 'password'">
<div v-if="showsPW" style="position: relative;">
<input type="text" v-model="setting.value">
<span class="material-symbols-outlined visibility" @click="togglePasswordVisibility()">visibility_off</span>
</div>
<div v-else style="position: relative;">
<input type="password" v-model="setting.value">
<span class="material-symbols-outlined visibility" @click="togglePasswordVisibility()">visibility</span>
</div>
</td>
<td v-else-if="setting.type == 'text'">
<input type="text" v-model="setting.value">
</td>
<td v-else-if="setting.type == 'textbox'">
<textarea v-model="setting.value"></textarea>
</td>
<td v-else-if="setting.type == 'date'">
<input type="date" v-model="setting.value">
</td>
<td v-else-if="setting.type == 'link'">
<router-link :to="setting.restrictions.to">{{ setting.restrictions.displayName }}</router-link>
</td>
</tr>
</table>
</div>
</template>
<script>
export default {
data () {
return {
showsPW: false,
};
},
props: {
settings: Object,
},
methods: {
togglePasswordVisibility () {
this.showsPW = !this.showsPW;
},
showInfo ( box ) {
$( '#' + box ).stop();
$( '#' + box ).fadeIn( 300 );
},
hideInfo ( box ) {
$( '#' + box ).stop();
$( '#' + box ).fadeOut( 300 );
}
}
};
</script>
<style scoped>
.visibility {
font-size: 100%;
cursor: pointer;
position: relative;
margin-left: 3px;
}
.settings-wrapper {
width: 100%;
display: flex;
text-align: justify;
align-items: center;
justify-content: center;
flex-direction: column;
}
.settings-toggles {
width: 70%;
}
.info-wrapper {
display: inline;
}
.info-container {
display: inline;
position: relative;
}
.info-icon {
font-size: 100%;
cursor: default;
}
.info-box {
text-align: center;
display: none;
position: absolute;
z-index: 10;
width: 20vw;
height: 20vh;
background-color: var( --hint-color );
border-radius: 20px;
top: 125%;
right: -9.3vw;
}
.info-box::before {
content: " ";
position: absolute;
bottom: 100%; /* At the bottom of the tooltip */
left: 50%;
margin-left: -5px;
border-width: 10px;
border-style: solid;
border-color: transparent transparent var( --hint-color ) transparent;
}
.info-box-container {
display: flex;
width: 80%;
height: 80%;
padding: 10%;
padding-top: 5%;
align-items: center;
justify-content: center;
}
/* https://www.w3schools.com/howto/tryit.asp?filename=tryhow_css_switch */
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
</style>

View File

@@ -1,150 +0,0 @@
<!--
* libreevent - sideCartView.vue
*
* Created by Janis Hutz 07/17/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div id="sideCartView" :class="type ? 'seatplan': 'noseatplan'">
<h1>Seat plan: {{ name }}</h1>
<h2>Cart</h2>
<div v-if="Object.keys( cart ).length > 0" style="height: 100%; width: 100%;">
<div class="scroll-wrapper">
<div v-for="event in cart">
<h3>{{ event.displayName }}</h3>
<table class="tickets-table">
<tr v-for="ticket in event.tickets">
<td>
<h4 class="price"><div style="display: inline;" v-if="ticket.count">{{ ticket.count }}x</div> {{ ticket.displayName }}: </h4>
</td>
<td>
{{ currency }} {{ ticket.price }}
</td>
</tr>
</table>
</div>
<table class="tickets-table">
<tr>
<td>
<h4>TOTAL:</h4>
</td>
<td>
<h4>{{ currency }} {{ total }}</h4>
</td>
</tr>
</table>
<router-link to="/cart" id="toCartButton">To Cart</router-link>
</div>
</div>
<div v-else>
Your cart is currently empty
</div>
</div>
</template>
<script>
export default {
name: 'sideCartView',
props: {
'cart': {
type: Object,
default: {}
// EXAMPLE: { 'TestEvent2': { 'displayName': 'TestEvent2', 'tickets': { 'secAr1s1': { 'displayName': 'Row 1, Seat 1', 'price': 20 } } } }
},
'width': {
type: Number,
default: 25
},
'currency': {
type: String,
default: 'CHF'
},
'type': {
type: Boolean,
default: true
},
'name': {
type: String,
default: ''
}
},
data() {
return {
total: 0,
};
},
methods: {
calculateTotal () {
this.total = 0;
for ( let event in this.cart ) {
for ( let ticket in this.cart[ event ][ 'tickets' ] ) {
this.total += parseInt( this.cart[ event ][ 'tickets' ][ ticket ][ 'price' ] ) * parseInt( this.cart[ event ][ 'tickets' ][ ticket ][ 'count' ] ?? 1 );
}
}
}
},
created() {
this.calculateTotal();
}
};
</script>
<style scoped>
#toCartButton {
text-decoration: none;
padding: 5%;
width: fit-content;
background-color: var( --accent-color );
color: var( --secondary-color );
transition: all 1s;
border-radius: 50px;
margin-top: 2%;
}
#toCartButton:hover {
background-color: var( --accent-background-hover );
border-radius: 10px;
}
#sideCartView {
background-color: var( --accent-background );
color: var( --secondary-color );
height: 50vh;
}
.seatplan {
width: 100%;
position: absolute;
top: calc( 90px + 80vh );
height: fit-content;
padding-bottom: 5%;
}
.tickets-table {
width: 80%;
}
.scroll-wrapper {
width: 100%;
height: 70%;
overflow: scroll;
}
.price {
margin: 0;
padding: 0;
}
@media only screen and (min-width: 999px) {
#sideCartView {
position: fixed;
right: 0;
height: 100vh;
top: 90px;
width: 25vw;
}
}
</style>

View File

@@ -1,47 +0,0 @@
/*
* libreevent - main.js
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { createPinia } from 'pinia';
import { useUserStore } from '@/stores/userStore';
let app = createApp( App );
app.use( createPinia() );
let userStore = useUserStore();
let prod = true;
if ( prod ) {
fetch( '/api/getAuth' ).then( res => {
// fetch( 'http://localhost:8081/api/getAuth' ).then( res => {
res.json().then( data => {
userStore.setUserAuth( data.user );
userStore.setAdminAuth( data.admin );
localStorage.setItem( 'url', '' );
fetch( '/getAPI/getName' ).then( res => {
res.json().then( data => {
userStore.setPageName( data.name );
app.use( router );
app.mount( '#app' );
} );
} );
} );
} );
} else {
localStorage.setItem( 'url', 'http://localhost:8080' );
userStore.setUserAuth( true );
userStore.setAdminAuth( true );
localStorage.setItem( 'name', 'libreevent' );
app.use( router );
app.mount( '#app' );
}

View File

@@ -1,93 +0,0 @@
/*
* libreevent - adminRoutes.js
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
export default {
path: '/admin',
name: 'admin',
component: () => import( '../views/admin/AdminView.vue' ),
meta: {
title: 'Admin - ',
adminAuthRequired: true,
},
children: [
{
path: '',
name: 'adminHome',
component: () => import( '../views/admin/HomeView.vue' ),
meta: {
title: 'Home :: Admin - ',
adminAuthRequired: true,
}
},
{
path: 'locations',
name: 'adminLocations',
component: () => import( '../views/admin/LocationsView.vue' ),
meta: {
title: 'Accounts :: Admin - ',
adminAuthRequired: true,
permissions: 'root'
}
},
{
path: 'pages',
name: 'adminPages',
component: () => import( '../views/admin/PagesView.vue' ),
meta: {
title: 'Pages :: Admin - ',
adminAuthRequired: true,
}
},
{
path: 'events',
name: 'adminEvents',
component: () => import( '../views/admin/EventsView.vue' ),
meta: {
title: 'Events :: Admin - ',
adminAuthRequired: true,
},
},
{
path: 'plugins',
name: 'adminPlugins',
component: () => import( '../views/admin/PluginsView.vue' ),
meta: {
title: 'Plugins :: Admin - ',
adminAuthRequired: true,
}
},
{
path: 'settings',
name: 'adminSettings',
component: () => import( '../views/admin/SettingsView.vue' ),
meta: {
title: 'Admin - ',
adminAuthRequired: true,
}
},
{
path: 'events/view',
name: 'eventDetails',
component: () => import( '../views/admin/events/EventsDetailsView.vue' ),
meta: {
title: 'Event details :: Admin - ',
adminAuthRequired: true,
}
},
{
path: 'events/analytics',
name: 'eventAnalytics',
component: () => import( '../views/admin/events/AnalyticsView.vue' ),
meta: {
title: 'Event analytics :: Admin - ',
adminAuthRequired: true,
}
},
]
};

View File

@@ -1,58 +0,0 @@
/*
* libreevent - index.js
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
import { createRouter, createWebHistory } from 'vue-router';
import { useUserStore } from '@/stores/userStore';
import adminRoutes from '@/router/adminRoutes';
import mainRoutes from '@/router/mainRoutes';
/*
This is the Vue.js router file. All valid routes get imported from other files to
improve code legibility. Only router logic and importing logic is defined here.
*/
const routes = mainRoutes;
routes.push( adminRoutes );
const router = createRouter( {
history: createWebHistory( import.meta.env.BASE_URL ),
routes,
} );
router.afterEach( ( to ) => {
let userStore = useUserStore();
document.title = to.meta.title ? to.meta.title + userStore.getPageName : 'libreevent';
} );
let UserAccountPages = [ 'account' ];
router.beforeEach( ( to ) => {
let userStore = useUserStore();
let isUserAuthenticated = userStore.getUserAuthenticated;
let isAdminAuthenticated = userStore.getAdminAuthenticated;
if ( to.meta.adminAuthRequired && !isAdminAuthenticated ) {
return { name: 'adminLogin' };
} else if ( to.name === 'adminLogin' && isAdminAuthenticated ) {
return { name: 'adminHome' };
} else if ( UserAccountPages.includes( to.name ) && !isUserAuthenticated ) {
return { name: 'login' };
} else if ( isUserAuthenticated && to.name === 'login' ) {
return { name: 'account' };
} else if ( to.name === '2fa' && !userStore.getUserTwoFACompliant ) {
return { name: 'login' };
} else if ( to.name === 'Admin2fa' && !userStore.getAdminTwoFACompliant ) {
return { name: 'adminLogin' };
}
} );
export default router;

View File

@@ -1,173 +0,0 @@
/*
* libreevent - mainRoutes.js
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
import HomeView from '../views/HomeView.vue';
/*
This file contains all the routes for all pages accessible to a normal
user. It also includes some pages that require sign in.
*/
export default [
{
path: '/',
name: 'home',
component: HomeView,
meta: {
title: 'Home - '
}
},
{
path: '/tickets',
name: 'tickets',
component: () => import( '../views/purchasing/OrderView.vue' ),
meta: {
title: 'Order ticket - '
}
},
{
path: '/login',
name: 'login',
component: () => import( '../views/user/LoginView.vue' ),
meta: {
title: 'Login - '
}
},
{
path: '/admin/login',
name: 'adminLogin',
component: () => import( '../views/admin/AdminLoginView.vue' ),
meta: {
title: 'Login :: Admin - '
}
},
{
path: '/admin/twoFactors',
name: 'admin2FA',
component: () => import( '../views/admin/TwoFA.vue' ),
meta: {
title: 'Two Factor Authentication :: Admin - '
}
},
{
path: '/signup',
name: 'signup',
component: () => import( '../views/user/SignupView.vue' ),
meta: {
title: 'Signup - '
}
},
{
path: '/account',
name: 'account',
component: () => import( '../views/user/AccountView.vue' ),
meta: {
title: 'Account - '
}
},
{
path: '/twoFactors',
name: '2fa',
component: () => import( '../views/user/TwoFA.vue' ),
meta: {
title: 'Two Factor Authentication - '
}
},
{
path: '/tickets/details',
name: 'ticketDetails',
component: () => import( '../views/purchasing/TicketsDetailsView.vue' ),
meta: {
title: 'Details - ',
transition: 'scale'
}
},
{
path: '/tickets/order',
name: 'ticketOrder',
component: () => import( '../views/purchasing/TicketsOrderingView.vue' ),
meta: {
title: 'Order ticket - ',
transition: 'scale'
}
},
{
path: '/cart',
name: 'cart',
component: () => import( '../views/purchasing/CartView.vue' ),
meta: {
title: 'Cart - ',
transition: 'scale'
}
},
{
path: '/purchase',
name: 'purchase',
component: () => import( '@/views/purchasing/PurchaseView.vue' ),
meta: {
title: 'Purchase - ',
transition: 'scale'
}
},
{
path: '/password-reset',
name: 'passwordReset',
component: () => import( '@/views/user/PasswordResetView.vue' ),
meta: {
title: 'Reset password - ',
transition: 'scale'
}
},
{
path: '/guest',
name: 'guestPurchase',
component: () => import( '@/views/purchasing/GuestPurchaseView.vue' ),
meta: {
title: 'Guest purchase - ',
transition: 'scale'
}
},
{
path: '/payments/success',
name: 'paymentSuccess',
component: () => import( '@/views/purchasing/PaymentSuccessView.vue' ),
meta: {
title: 'Payment successful - ',
transition: 'scale'
}
},
{
path: '/admin/seatplan',
name: 'adminSeatplanEditor',
component: () => import( '@/views/admin/events/EditorView.vue' ),
meta: {
title: 'Seatplan Editor :: Admin - ',
adminAuthRequired: true,
}
},
{
path: '/admin/ticketEditor',
name: 'adminTicketEditor',
component: () => import( '@/views/admin/events/TicketEditorView.vue' ),
meta: {
title: 'Ticket Editor :: Admin - ',
adminAuthRequired: true,
}
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import( '@/views/404.vue' ),
meta: {
title: '404 - Page not found :: ',
transition: 'scale',
}
},
];

View File

@@ -1,21 +0,0 @@
/*
* libreevent - backendStore.js
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
import { defineStore } from 'pinia';
export const useBackendStore = defineStore( 'backend', {
state: () => ( { 'guestPurchase': false, 'guestPurchaseAllowed': false } ),
getters: {
getIsGuestPurchase: ( state ) => state.guestPurchase,
getIsGuestPurchaseAllowed: ( state ) => state.guestPurchaseAllowed,
},
// actions: {
// }
} );

View File

@@ -1,38 +0,0 @@
/*
* libreevent - userStore.js
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
import { defineStore } from 'pinia';
export const useUserStore = defineStore( 'user', {
state: () => ( { 'isUserAuth': false, 'isAdminAuth': false, 'userData': {}, 'isTwoFACompliantUser': false, 'isTwoFACompliantAdmin': false, 'pageName': 'libreevent' } ),
getters: {
getUserAuthenticated: ( state ) => state.isUserAuth,
getAdminAuthenticated: ( state ) => state.isAdminAuth,
getUserTwoFACompliant: ( state ) => state.isTwoFACompliantUser,
getAdminTwoFACompliant: ( state ) => state.isTwoFACompliantAdmin,
getPageName: ( state ) => state.pageName,
},
actions: {
setUserAuth ( auth ) {
this.isUserAuth = auth;
},
setAdminAuth ( auth ) {
this.isAdminAuth = auth;
},
setUser2fa ( auth ) {
this.isTwoFACompliantUser = auth;
},
setAdmin2fa ( auth ) {
this.isTwoFACompliantAdmin = auth;
},
setPageName ( name ) {
this.pageName = name;
}
}
} );

View File

@@ -1,48 +0,0 @@
<!--
* libreevent - 404.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="notFound">
<h1 class="code">404</h1>
<h2 class="message">The page you are looking for was not found on the server!</h2>
<router-link to="/">Return to home page</router-link>
<a href="https://libreevent.janishutz.com/docs/errors#404" target="_blank">More information on this error</a>
</div>
</template>
<style scoped>
.notFound {
font-family: monospace;
color: var( --primary-color );
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 100%;
width: 100%;
}
.code {
font-size: 2500%;
margin: 0;
padding: 0;
}
.message {
font-size: 200%;
}
.small {
font-style: italic;
}
nav {
display: initial;
}
</style>

View File

@@ -1,29 +0,0 @@
<!--
* libreevent - HomeView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h1>Welcome!</h1>
</div>
</template>
<script>
export default {
data () {
return {
formData: {}
};
},
methods: {
setup () {
}
},
};
</script>

View File

@@ -1,129 +0,0 @@
<!--
* libreevent - AdminLoginView.vue
*
* Created by Janis Hutz 07/16/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="login">
<div class="login-app">
<h1>Log into your admin account</h1>
<form>
<label for="mail">Email</label><br>
<input type="email" v-model="formData[ 'mail' ]" name="mail" id="mail" required><br><br>
<label for="password">Password</label><br>
<input type="password" v-model="formData[ 'password' ]" name="password" id="password" required>
</form>
<button @click="login();" class="button">Log in</button>
</div>
<notifications ref="notification" location="topright" size="bigger"></notifications>
</div>
</template>
<script>
import { useUserStore } from '@/stores/userStore';
import { mapStores } from 'pinia';
import notifications from '@/components/notifications/notifications.vue';
export default {
data () {
return {
formData: {}
};
},
components: {
notifications,
},
computed: {
...mapStores( useUserStore )
},
methods: {
login () {
if ( this.formData.mail ) {
if ( this.formData.password ) {
let progress = this.$refs.notification.createNotification( 'Logging you in', 20, 'progress', 'normal' );
let fetchOptions = {
method: 'post',
body: JSON.stringify( this.formData ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/admin/auth', fetchOptions ).then( res => {
res.json().then( json => {
if ( json.status === 'ok' ) {
this.userStore.setAdminAuth( true );
this.$router.push( '/admin' );
sessionStorage.removeItem( 'redirect' );
} else if ( json.status === '2fa' ) {
this.userStore.setAdmin2fa( true );
this.$router.push( '/admin/twoFactors' );
} else if ( json.status === '2fa+' ) {
this.userStore.setAdmin2fa( true );
sessionStorage.setItem( '2faCode', json.code );
this.$router.push( '/admin/twoFactors' );
} else {
this.$refs.notification.cancelNotification( progress );
this.$refs.notification.createNotification( 'The credentials you provided do not match our records.', 5, 'error', 'normal' );
}
} );
} );
} else {
this.$refs.notification.createNotification( 'A password is required to log in', 5, 'error', 'normal' );
}
} else {
this.$refs.notification.createNotification( 'An email address is required to log in', 5, 'error', 'normal' );
}
},
},
};
</script>
<style scoped>
.login {
background-image: url( '/otherAssets/background-login.webp' );
background-size: cover;
background-position: center;
width: 100%;
height: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 0 1 auto;
}
.login-app {
background-color: var( --background-color );
min-height: fit-content;
min-height: fit-content;
padding: 5% 20%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 50px;
}
.button {
padding: 5px 10px;
margin-top: 2%;
}
#missing-email, #missing-password, #credentials-wrong {
display: none;
margin-bottom: 20px;
}
</style>
<style>
nav {
display: initial;
}
</style>

View File

@@ -1,273 +0,0 @@
<!-- eslint-disable no-undef -->
<!--
* libreevent - AdminView.vue
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="admin-wrapper">
<div class="top-bar">
<h1>Admin panel</h1>
</div>
<button class="hideNav navHidden" @click="navMenu( 'toggle' );">&#9776;</button>
<nav class="side-nav">
<div class="side-nav-wrapper">
<img src="@/assets/logo.png" alt="libreevent logo" style="width: 80%; margin-left: 10%; margin-bottom: 5%;">
<router-link to="/admin" class="admin-menu" @click="navMenu( 'hide' )" title="The home page of the admin panel">Home</router-link>
<router-link to="/admin/pages" class="admin-menu" @click="navMenu( 'hide' )" title="Modify your landing page">Pages</router-link>
<router-link to="/admin/events" class="admin-menu" @click="navMenu( 'hide' )" title="Change and view everything about your events">Events</router-link>
<router-link to="/admin/locations" class="admin-menu" @click="navMenu( 'hide' )" title="Change settings about your event locations">Locations</router-link>
<router-link to="/admin/plugins" class="admin-menu" @click="navMenu( 'hide' )" title="Manage plugins">Plugins</router-link>
<router-link to="/admin/settings" class="admin-menu" @click="navMenu( 'hide' )" title="Change global settings for libreevent">Settings</router-link>
<button to="/admin/login" class="admin-menu" @click="logout()" title="Log out of the admin panel">Logout</button>
</div>
</nav>
<div class="backdrop" @click="navMenu( 'hide' )"></div>
<div class="main-view">
<router-view v-slot="{ Component, route }">
<transition :name="route.meta.transition || 'fade'" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</div>
</template>
<style scoped>
.admin-wrapper {
display: block;
height: 100%;
}
.top-bar {
display: flex;
justify-content: center;
align-items: center;
height: 10vh;
}
.main-view {
grid-area: main;
height: 90vh;
width: 100vw;
min-height: 80vh;
overflow: scroll;
}
.backdrop {
width: 100vw;
height: 100vh;
z-index: 1;
display: none;
position: fixed;
top: 0;
left: 0;
background-color: rgba( 0, 0, 0, 0.7 );
}
.side-nav {
position: fixed;
display: none;
height: 100vh;
top: 0;
width: 100vw;
padding: 0;
margin: 0;
background-color: var( --accent-background );
z-index: 3;
}
.side-nav-wrapper {
display: flex;
flex-direction: column;
justify-content: center;
overflow: scroll;
height: 100%;
}
.admin-menu {
padding: 4% 0%;
width: 100%;
background-color: rgba( 0, 0, 0, 0 );
color: var( --secondary-color );
cursor: pointer;
font-family: Avenir, Helvetica, Arial, sans-serif;
font-weight: bold;
text-decoration: none;
transition: 1s;
font-size: 100%;
border-style: none;
}
nav a.router-link-exact-active {
background-color: var( --accent-background-hover );
}
.admin-menu:hover {
background-color: var( --accent-background-hover );
transition: 0.4s;
}
.hideNav {
border: none;
font-size: 300%;
color: var( --secondary-color );
width: fit-content;
background-color: rgba( 0, 0, 0, 0 );
position: fixed;
cursor: pointer;
top: 1vw;
left: 2vw;
z-index: 5;
}
.navHidden {
color: var( --primary-color );
}
@media only screen and (min-width: 400px) {
.side-nav {
width: 70vw;
}
}
@media only screen and (min-width: 750px) {
.side-nav {
width: 45vw;
}
}
@media only screen and (min-width: 1050px) {
.side-nav {
width: 32vw;
}
}
@media only screen and (min-width: 1300px) {
.hideNav, .backdrop {
display: none;
}
.admin-wrapper {
display: grid;
grid-template-areas:
'sidebar top top top'
'sidebar main main main'
'sidebar main main main'
'sidebar main main main'
'sidebar main main main'
'sidebar main main main'
'sidebar main main main'
'sidebar main main main'
'sidebar main main main';
height: 100%;
}
.top-bar {
grid-area: top;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.main-view {
grid-area: main;
height: 100%;
width: 80vw;
min-height: 80vh;
overflow: scroll;
}
.side-nav {
position: initial;
grid-area: sidebar;
display: flex;
flex-direction: column;
height: 100%;
width: 20vw;
padding: 0;
margin: 0;
background-color: var( --accent-background );
justify-content: center;
overflow: scroll;
}
.admin-menu {
padding: 4% 0%;
width: 100%;
background-color: rgba( 0, 0, 0, 0 );
color: var( --secondary-color );
cursor: pointer;
font-family: Avenir, Helvetica, Arial, sans-serif;
font-weight: bold;
text-decoration: none;
transition: 1s;
font-size: 100%;
border-style: none;
}
nav a.router-link-exact-active {
background-color: var( --accent-background-hover );
}
.admin-menu:hover {
background-color: var( --accent-background-hover );
transition: 0.4s;
}
}
</style>
<style>
nav {
display: none;
}
</style>
<script>
import { useUserStore } from '@/stores/userStore';
import { mapStores } from 'pinia';
export default {
data () {
return {
formData: {}
};
},
computed: {
...mapStores( useUserStore )
},
methods: {
logout () {
if ( confirm( 'Do you really want to log out?' ) ) {
fetch( '/admin/logout' ).then( () => {
this.userStore.setAdminAuth( false );
this.$router.push( '/admin/login' );
} );
}
},
navMenu ( action ) {
if ( window.screen.width < 1300 ) {
if ( action === 'toggle' ) {
$( '.side-nav' ).toggle( 300 );
$( '.hideNav' ).toggleClass( 'navHidden' );
$( '.backdrop' ).toggle( 300 );
} else if ( action === 'show' ) {
$( '.backdrop' ).show( 300 );
$( '.side-nav' ).show( 300 );
$( '.hideNav' ).removeClass( 'navHidden' );
} else {
$( '.side-nav' ).hide( 300 );
$( '.backdrop' ).hide( 300 );
$( '.hideNav' ).addClass( 'navHidden' );
}
}
}
}
};
</script>

View File

@@ -1,217 +0,0 @@
<!--
* libreevent - EventsView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="order">
<h2>Events</h2>
<button @click="addEvent()">Create new event</button>
<div class="order-app" v-if="events">
<ul v-for="timeframe in eventList">
<h3>{{ timeframe.name }}</h3>
<li v-for="event in timeframe.content" @contextmenu="( e ) => { e.preventDefault(); openRightClickMenu( event.eventID, e ); }">
<router-link to="/admin/events/view" class="ticket" @click="setActiveTicket( event.eventID );" v-if="new Date( event.date ).getTime() > currentDate || timeframe.name === 'Drafts'">
<div class="ticket-name">
<h3>{{ event.name }}</h3>
<p>{{ event.shortDescription }}</p>
<b>{{ event.date }}</b>
</div>
<!-- <img :src="event.logo" alt="event logo" class="ticket-logo"> -->
</router-link>
<router-link to="/admin/events/analytics" class="ticket" @click="setActiveTicket( event.eventID );" v-else>
<div class="ticket-name">
<h3>{{ event.name }}</h3>
<p>{{ event.description }}</p>
<b>{{ event.date }}</b>
</div>
<!-- <img :src="event.logo" alt="event logo" class="ticket-logo"> -->
</router-link>
</li>
</ul>
</div>
<div class="order-app" v-else>
No events are available!
</div>
<popups ref="popup" size="big" @data="( data ) => {
handleData( data );
}"></popups>
<rightClickMenu ref="rclk" @command="( command ) => { executeCommand( command ) }"></rightClickMenu>
</div>
</template>
<style scoped>
.order-app {
text-align: justify;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
ul {
list-style: none;
width: 80%;
}
.ticket {
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
color: var( --primary-color );
border-color: var( --primary-color );
border-width: 1px;
height: fit-content;
border-style: solid;
padding: 10px;
transition: 0.4s;
}
.ticket:hover {
background-color: var( --hover-color );
transition: 0.4s;
}
.ticket-logo {
height: 20vh;
width: auto;
margin-left: auto;
}
.ticket-name {
margin-right: auto;
max-width: 60%;
}
.ticket-info {
margin-left: auto;
margin-right: auto
}
</style>
<script>
import popups from '@/components/notifications/popups.vue';
import rightClickMenu from '@/components/settings/rightClickMenu.vue';
export default {
name: 'OrderView',
components: {
popups,
rightClickMenu,
},
data () {
return {
events: { 'test': { 'name': 'TestEvent', 'description': 'This is a description for the TestEvent to test multiline support and proper positioning of the Fields', 'freeSeats': 2, 'maxSeats': 2, 'date': '2023-08-15', 'startingPrice': 15, 'location': 'TestLocation', 'eventID': 'test', 'currency': 'CHF', 'logo': new URL( '/assets/logo.png', import.meta.url ).href } },
currentDate: new Date().getTime(),
eventList: { 'upcoming': { 'name': 'Upcoming', 'content': {} }, 'past': { 'name': 'Past', 'content': {} }, 'drafts': { 'name': 'Drafts', 'content': {} } },
currentlyOpenMenu: '',
};
},
created() {
this.loadData();
},
methods: {
loadData () {
fetch( '/admin/getAPI/getAllEvents' ).then( res => {
res.json().then( dat => {
this.events = dat[ 'live' ] ?? {};
this.eventList.drafts[ 'content' ] = dat[ 'drafts' ] ?? {};
let sortable = [];
for ( let event in this.events ) {
if ( this.events[ event ][ 'description' ].length > 200 ) {
this.events[ event ][ 'shortDescription' ] = this.events[ event ][ 'description' ].slice( 0, 200 ) + '...';
} else {
this.events[ event ][ 'shortDescription' ] = this.events[ event ][ 'description' ];
}
sortable.push( [ this.events[ event ][ 'eventID' ], new Date( this.events[ event ][ 'date' ] ).getTime() ] );
}
sortable.sort( function( a, b ) {
return a[ 1 ] - b[ 1 ];
} );
for ( let element in sortable ) {
if ( this.eventList.drafts[ 'content' ][ sortable[ element ][ 0 ] ] ) {
delete this.eventList.drafts[ 'content' ][ sortable[ element ][ 0 ] ];
}
if ( sortable[ element ][ 1 ] > this.currentDate ) {
this.eventList.upcoming.content[ sortable[ element ][ 0 ] ] = this.events[ sortable[ element ][ 0 ] ];
} else {
this.eventList.past.content[ sortable[ element ][ 0 ] ] = this.events[ sortable[ element ][ 0 ] ];
}
}
} );
} );
},
openRightClickMenu( id, event ) {
this.$refs.rclk.openRightClickMenu( event, { 'edit': { 'command': 'editEvent', 'symbol': 'edit', 'display': 'Edit event' }, 'delete': { 'command': 'deleteEvent', 'symbol': 'delete', 'display': 'Delete event' } } );
this.currentlyOpenMenu = id;
},
executeCommand( command ) {
if ( command === 'editEvent' ) {
sessionStorage.setItem( 'selectedTicket', this.currentlyOpenMenu );
this.$router.push( '/admin/events/view' );
} else if ( command === 'deleteEvent' ) {
this.$refs.popup.openPopup( 'Do you really want to delete the event ' + this.currentlyOpenMenu + '?', {}, 'confirm' );
this.currentPopup = 'delete';
}
},
addEvent () {
this.currentPopup = 'add';
this.$refs.popup.openPopup( 'Please give the new event a name for internal use', { 'disallowedCharacters': [ '_', '-' ] }, 'text' );
},
setActiveTicket ( id ) {
sessionStorage.setItem( 'selectedTicket', id );
},
handleData ( data ) {
if ( this.currentPopup === 'add' ) {
if ( data.status === 'ok' ) {
const options = {
method: 'post',
body: JSON.stringify( { 'event': data.data } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/admin/api/createEvent', options ).then( res => {
if ( res.status === 200 ) {
res.text().then( () => {
this.currentlyOpenMenu = '';
this.loadData();
} );
} else if ( res.status === 409 ) {
this.$refs.popup.openPopup( 'This event does already exist. Please choose a different identifier!', {}, 'string' );
}
} );
}
} else if ( this.currentPopup === 'delete' ) {
console.log( data );
if ( data.status === 'ok' ) {
const options = {
method: 'post',
body: JSON.stringify( { 'event': this.currentlyOpenMenu } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/admin/api/deleteEvent', options ).then( res => {
if ( res.status === 200 ) {
res.text().then( () => {
this.currentlyOpenMenu = '';
this.loadData();
} );
}
} );
}
}
},
}
};
</script>

View File

@@ -1,52 +0,0 @@
<!--
* libreevent - HomeView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h2>Home</h2>
<p>Welcome to the admin panel!</p>
<div class="bigButton-container">
<router-link to="/admin/pages" class="bigButton">Pages</router-link>
<router-link to="/admin/events" class="bigButton">Events</router-link>
<router-link to="/admin/locations" class="bigButton">Locations</router-link>
<router-link to="/admin/plugins" class="bigButton">Plugins</router-link>
<router-link to="/admin/settings" class="bigButton">Settings</router-link>
<a href="https://libreevent.janishutz.com/docs/admin-panel" class="bigButton" target="_blank">Documentation <span class="material-symbols-outlined" style="display: inline; font-size: 100%;">open_in_new</span></a>
</div>
</div>
</template>
<style scoped>
.bigButton-container {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: center;
}
.bigButton {
background-color: var( --accent-background );
width: 40%;
height: 30vh;
border-color: var( --accent-background-hover );
border-style: solid;
color: var( --secondary-color );
text-decoration: none;
border-width: 2px;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
}
.bigButton:hover {
background-color: var( --accent-background-hover );
}
</style>

View File

@@ -1,243 +0,0 @@
<!--
* libreevent - LocationsView.vue
*
* Created by Janis Hutz 06/05/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h2>Locations</h2>
<p>Here you can change everything regarding event locations. All locations can have a seating plan. Right click for more options.</p>
<button @click="addLocation();">Add new location</button>
<div class="location-app" v-if="Object.keys( locations ).length">
<ul>
<li v-for="location in locations">
<div class="location" @click="selectLocation( location.locationID );" title="Edit this location" @contextmenu="( e ) => { e.preventDefault(); openRightClickMenu( location.locationID, e, location['seatplan-enabled'] ); }">
<div class="location-name">
<h3>{{ location.locationID }} ({{ location.name }})</h3>
<p v-if="location['seatplan-enabled']">This location has a seatplan.</p>
<p v-else>This location has NO seatplan.</p>
</div>
</div>
</li>
</ul>
</div>
<div v-else class="no-location-hint">
No locations configured, please <b @click="addLocation();" style="cursor: pointer;">add</b> one
</div>
<popups ref="popup" size="big" @data="( data ) => {
handleData( data );
}"></popups>
<rightClickMenu ref="rclk" @command="( command ) => { executeCommand( command ) }"></rightClickMenu>
</div>
</template>
<script>
import popups from '@/components/notifications/popups.vue';
import rightClickMenu from '@/components/settings/rightClickMenu.vue';
export default {
data () {
return {
locations: { 'test': { 'name': 'TestLocation', 'locationID': 'test', 'seatplan-enabled': true, 'seatplan': {} } },
currentlyOpenMenu: '',
currentPopup: '',
updatedLocations: {}
};
},
components: {
popups,
rightClickMenu,
},
methods: {
selectLocation ( locationID ) {
sessionStorage.setItem( 'locationID', locationID );
this.currentlyOpenMenu = locationID;
this.$refs.popup.openPopup( 'Settings for ' + this.locations[ locationID ][ 'name' ], {
'locationID': {
'display': 'Internal location name',
'id': 'locationID',
'tooltip': 'Give the location where the event takes place a name. This name will not be shown to the customers and is used for the backend and admin portal. Has to be unique',
'value': locationID,
'type': 'text',
},
'name': {
'display': 'Public location name',
'id': 'name',
'tooltip': 'The name of the location that is shown to the customers.',
'value': this.locations[ locationID ][ 'name' ],
'type': 'text',
},
'seatplan-enabled': {
'display': 'Use seat plan?',
'id': 'seatplan-enabled',
'tooltip': 'With this toggle you may specify whether or not this location has a seat plan or not.',
'value': this.locations[ locationID ][ 'seatplan-enabled' ],
'type': 'toggle',
},
}
, 'settings' );
},
addLocation () {
this.$refs.popup.openPopup( 'Add a new location', {
'locationID': {
'display': 'Internal location name',
'id': 'locationID',
'tooltip': 'Give the location where the event takes place a name. This name will not be shown to the customers and is used for the backend and admin portal. Has to be unique',
'value': '',
'type': 'text',
},
'name': {
'display': 'Public location name',
'id': 'name',
'tooltip': 'The name of the location that is shown to the customers.',
'value': '',
'type': 'text',
},
'seatplan-enabled': {
'display': 'Use seat plan?',
'id': 'seatplan-enabled',
'tooltip': 'With this toggle you may specify whether or not this location has a seat plan or not.',
'value': true,
'type': 'toggle',
},
'seatplanEditor': {
'display': 'Seat plan editor',
'id': 'seatplanEditor',
'tooltip': 'The seat plan editor allows you to create a seat plan that closely resembles the location you host the event in.',
'type': 'link',
'restrictions': {
'to': '/admin/seatplan',
'displayName': 'Edit seat plan'
}
},
}
, 'settings' );
},
openRightClickMenu( id, event, hasSeatplan ) {
this.currentlyOpenMenu = id;
if ( hasSeatplan ) {
this.$refs.rclk.openRightClickMenu( event, { 'edit': { 'command': 'editLocation', 'symbol': 'edit', 'display': 'Edit location' }, 'editor': { 'command': 'openEditor', 'symbol': 'tune', 'display': 'Edit seatplan' }, 'delete': { 'command': 'deleteLocation', 'symbol': 'delete', 'display': 'Delete location' } } );
} else {
this.$refs.rclk.openRightClickMenu( event, { 'edit': { 'command': 'editLocation', 'symbol': 'edit', 'display': 'Edit location' }, 'delete': { 'command': 'deleteLocation', 'symbol': 'delete', 'display': 'Delete location' } } );
}
},
executeCommand( command ) {
if ( command === 'editLocation' ) {
this.selectLocation( this.currentlyOpenMenu );
} else if ( command === 'deleteLocation' ) {
this.$refs.popup.openPopup( 'Do you really want to delete the location ' + this.currentlyOpenMenu + '?', {}, 'confirm' );
this.currentPopup = 'delete';
} else if ( command === 'openEditor' ) {
sessionStorage.setItem( 'locationID', this.currentlyOpenMenu );
this.$router.push( '/admin/seatplan' );
}
},
handleData ( data ) {
if ( this.currentPopup === 'delete' ) {
this.currentPopup = '';
if ( data.status === 'ok' ) {
delete this.locations[ this.currentlyOpenMenu ];
const options = {
method: 'post',
body: JSON.stringify( { 'location': this.currentlyOpenMenu } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/admin/api/deleteLocation', options ).then( res => {
if ( res.status === 200 ) {
res.text().then( text => {
console.log( text );
} );
}
} );
}
} else {
if ( data.status === 'settings' ) {
if ( data.data.locationID !== this.currentlyOpenMenu && this.currentlyOpenMenu !== '' ) {
delete this.locations[ this.currentlyOpenMenu ];
this.updatedLocations[ this.currentlyOpenMenu ] = data.data.locationID;
}
this.locations[ data.data.locationID ] = data.data;
this.currentlyOpenMenu = '';
const options = {
method: 'post',
body: JSON.stringify( { 'updated': this.updatedLocations, 'data': this.locations } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/admin/api/saveLocations', options ).then( res => {
if ( res.status === 200 ) {
res.text().then( text => {
console.log( text );
} );
}
} );
}
}
},
},
created () {
fetch( localStorage.getItem( 'url' ) + '/admin/getAPI/getLocations' ).then( res => {
res.json().then( data => {
this.locations = data;
} ).catch( error => {
console.error( error );
} );
} );
}
};
</script>
<style scoped>
.location-app {
text-align: justify;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
ul {
list-style: none;
width: 80%;
}
.location {
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
color: var( --primary-color );
border-color: var( --primary-color );
border-width: 1px;
height: fit-content;
border-style: solid;
padding: 10px;
transition: 0.4s;
cursor: pointer;
}
.location:hover {
background-color: var( --hover-color );
transition: 0.4s;
}
.location-name {
margin-right: auto;
max-width: 35%;
}
.no-location-hint {
margin-top: 5%;
}
</style>

View File

@@ -1,225 +0,0 @@
<!--
* libreevent - PagesView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h2>Pages</h2>
<p>Here you can modify your landing page (the start page of libreǝvent)</p>
<h3>Select the template (see all <a href="https://libreevent.janishutz.com/docs/homepage/templates" target="_blank">here</a>)</h3>
<select name="templateSel" id="templateSel" v-model="selectedTemplate">
<option v-for="el in startPageTemplates" :value="el">{{ el }}</option>
</select>
<div>
<h4>Upload your website's logo here (png image)</h4>
<picture-input
ref="logoUpload"
width="350"
height="350"
:removable="false"
removeButtonClass="ui red button"
accept="image/png"
buttonClass="ui button primary"
:customStrings="{
upload: '<h1>Upload your image!</h1>',
drag: 'Drag and drop your image here'
}">
</picture-input><br>
<button @click="saveLogo()" class="button">Upload logo</button>
</div>
<h3>Change the settings of the start page here</h3>
<button @click="save()" class="button">Save</button>
<!-- Start page settings -> Defined by startPage.json file -->
<div class="start-page-settings">
<div class="setting" v-for="setting in startPageSettings">
<div v-if="setting.type === 'text'">
<p>{{ setting.display }}</p>
<input type="text" name="t" :id="setting.id" v-model="setting.value">
</div>
<div v-else-if="setting.type === 'textarea'">
<p>{{ setting.display }}</p>
<textarea name="textarea" :id="setting.id" :cols="setting[ 'textarea-settings' ].cols" :rows="setting[ 'textarea-settings' ].rows" v-model="setting.value"></textarea>
</div>
<div v-else-if="setting.type === 'image'">
<p>{{ setting.display }}</p>
<picture-input
:ref="setting.id"
:width="setting[ 'image-settings' ].width"
:height="setting[ 'image-settings' ].height"
:removable="false"
removeButtonClass="ui red button"
:accept="setting[ 'image-settings' ][ 'accept-filetype' ]"
buttonClass="ui button primary"
:customStrings="{
upload: '<h1>Upload your image!</h1>',
drag: 'Drag and drop your image here'
}">
</picture-input>
</div>
</div>
</div>
<button @click="save()" class="button">Save</button>
<button @click="enablePage()" class="button">Deploy page</button>
<notifications ref="notification" location="topright" size="bigger"></notifications>
<popups ref="popups" size="normal" @data="( data ) => { handlePopup( data ) }"></popups>
</div>
</template>
<script>
import PictureInput from 'vue-picture-input';
import notifications from '@/components/notifications/notifications.vue';
import popups from '@/components/notifications/popups.vue';
export default {
data () {
return {
startPageTemplates: [],
startPageSettings: {},
selectedTemplate: '',
};
},
components: {
PictureInput,
notifications,
popups,
},
methods: {
loadPageSettings() {
fetch( '/admin/getAPI/getStartPageSettings?name=' + this.selectedTemplate ).then( res => {
if ( res.status === 200 ) {
res.json().then( json => {
this.startPageSettings = json[ 'options' ];
for ( let option in this.startPageSettings ) {
this.startPageSettings[ option ][ 'value' ] = json[ 'conf' ][ option ];
}
} );
}
} );
},
save() {
let settings = {};
for ( let setting in this.startPageSettings ) {
if ( this.startPageSettings[ setting ][ 'type' ] === 'image' ) {
if ( this.saveImage( this.startPageSettings[ setting ].id ) ) {
this.$refs.notification.createNotification( 'No image selected!', 5, 'error', 'normal' );
}
} else {
if ( this.startPageSettings[ setting ][ 'value' ] ) {
settings[ setting ] = this.startPageSettings[ setting ];
} else {
this.$refs.notification.createNotification( 'Required entries are missing!', 10, 'error', 'normal' );
return;
}
}
}
const options = {
method: 'post',
body: JSON.stringify( { 'preferences': settings, 'page': this.selectedTemplate } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/admin/API/savePageSettings', options ).then( res => {
if ( res.status === 200 ) {
this.$refs.notification.createNotification( 'Saved settings successfully!', 5, 'ok', 'normal' );
} else {
this.$refs.notification.createNotification( 'An error occurred whilst saving', 10, 'error', 'normal' );
}
} );
},
enablePage() {
this.$refs.popups.openPopup( 'This operation will build the currently selected start page, enable it for use and overwrite any existing start pages.', {}, 'string' );
},
handlePopup( data ) {
if ( data.status === 'ok' ) {
const deploy = this.$refs.notification.createNotification( 'Building & deploying page...', 60, 'progress', 'normal' );
fetch( '/admin/getAPI/buildStartPage?page=' + this.selectedTemplate ).then( res => {
if ( res.status === 200 ) {
this.$refs.notification.cancelNotification( deploy );
this.$refs.notification.createNotification( 'Start page has been deployed successfully!', 5, 'ok', 'normal' );
} else if ( res.status === 412 ) {
this.$refs.notification.cancelNotification( deploy );
this.$refs.notification.createNotification( 'Some required fields for the page are missing. Did you hit save before clicking here?', 10, 'error', 'normal' );
} else {
console.error( res );
this.$refs.notification.cancelNotification( deploy );
this.$refs.notification.createNotification( 'An unknown error occurred whilst processing the request. Please try again later.', 5, 'error', 'normal' );
}
} );
}
},
saveImage( image ) {
if ( this.$refs[ image ][ 0 ].file ) {
console.log( 'saving image' );
let fd = new FormData();
fd.append( 'image', this.$refs[ image ][ 0 ].file );
let fetchOptions = {
method: 'post',
body: fd,
};
fetch( localStorage.getItem( 'url' ) + '/admin/pages/uploadImages?image=' + image + '&template=' + this.selectedTemplate, fetchOptions ).then( res => {
if ( res.status === 200 ) {
return true;
} else {
this.$refs.notification.createNotification( 'There was an error uploading the image', 5, 'error', 'normal' );
}
} ).catch( err => {
console.error( err );
} );
return true;
} else {
return false;
}
},
saveLogo() {
if ( this.$refs.logoUpload.file ) {
let fd = new FormData();
fd.append( 'image', this.$refs.logoUpload.file );
let fetchOptions = {
method: 'post',
body: fd,
};
fetch( localStorage.getItem( 'url' ) + '/admin/logo/upload', fetchOptions ).then( res => {
if ( res.status === 200 ) {
this.$refs.notification.createNotification( 'Logo uploaded successfully', 5, 'ok', 'normal' );
} else {
this.$refs.notification.createNotification( 'There was an error uploading the image', 5, 'error', 'normal' );
}
} ).catch( err => {
console.error( err );
} );
} else {
this.$refs.notification.createNotification( 'No logo selected. Please select one and try again!', 10, 'error', 'normal' );
}
}
},
watch: {
selectedTemplate() {
this.loadPageSettings();
}
},
created () {
fetch( '/admin/getAPI/getAllStartPages' ).then( res => {
if ( res.status === 200 ) {
res.json().then( json => {
this.startPageTemplates = json;
} );
}
} );
fetch( '/admin/getAPI/getSettings' ).then( res => {
if ( res.status === 200 ) {
res.json().then( json => {
this.selectedTemplate = json[ 'startPage' ];
} );
}
} );
}
};
</script>

View File

@@ -1,86 +0,0 @@
<!--
* libreevent - PluginsView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h2>Plugins</h2>
<p>Here you can manage installed plugins. If you want to install more plugins, please follow the guide <a href="https://librevent.janishutz.com/docs/plugins/install">here</a>. Note that some plugins might not follow the libreevent design scheme.</p>
<div class="bigButton-container">
<a class="bigButton" v-for="plugin in allPlugins" :href="plugin.settingsURL">
<object data="/otherAssets/libreeventLogo.png" type="image/png" class="plugin-logo" @click="$router.push( plugin.settingsURL )">
<img :src="plugin.logo" @click="$router.push( plugin.settingsURL )" style="cursor: pointer;">
</object>
<h3 style="margin-bottom: 0;">{{ plugin.pluginName }}</h3>
<p>{{ plugin.pluginDescription }}</p>
<p style="margin: 0">(Version V{{ plugin.version }}, maintained by {{ plugin.maintainer }})</p>
</a>
</div>
</div>
</template>
<script>
export default {
data () {
return {
allPlugins: {}
};
},
methods: {
loadData () {
fetch( '/admin/getAPI/getAllPlugins' ).then( res => {
if ( res.status === 200 ) {
res.json().then( json => {
this.allPlugins = json;
} ).catch( err => {
console.error( err );
} );
}
} );
}
},
created () {
this.loadData();
}
};
</script>
<style scoped>
.plugin-logo {
height: 50%;
margin: 0;
cursor: pointer;
}
.bigButton-container {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: center;
}
.bigButton {
background-color: var( --accent-background );
width: 40%;
height: 40vh;
border-color: black;
margin: 0.02%;
border-style: inset;
color: var( --secondary-color );
text-decoration: none;
border-width: 2px;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
}
.bigButton:hover {
background-color: var( --accent-background-hover );
}
</style>

View File

@@ -1,502 +0,0 @@
<!--
* libreevent - SettingsView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<!-- <h2>Setup check</h2> -->
<!-- TODO: FUTURE add -->
<!-- call config check of payment + check if events are deployed -->
<h2>Settings</h2>
<p>Changing any of these settings requires a restart of libreevent.</p>
<p>Currency codes used must be valid ISO 4217 codes. Read more on <a href="https://libreevent.janishutz.com/docs/admin-panel/settings#currency" target="_blank">this page</a> of the documentation <!-- https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes"--></p>
<settings v-model:settings="settings"></settings>
<button @click="save()">Save</button>
<p>Detailed explanation of payment gateways can be found <a href="https://libreevent.janishutz.com/docs/payments" target="_blank">here</a>. Please note that you need to save the settings before you can edit settings of the payment gateway after changing it.</p>
<table class="gateway-settings">
<tr>
<td style="width: 75%;">
Edit settings of the payment gateway
</td>
<td>
<span class="material-symbols-outlined" @click="showPaymentSettings();" style="cursor: pointer;" title="Edit settings for the payment gateway you selected">settings</span>
</td>
</tr>
</table>
<div class="admin-settings">
<h2>Admin Accounts</h2>
<button @click="createAccount()">Create new account</button>
<!-- <p style="margin-bottom: 0;">Before setting or editing permissions here, please read the corresponding section of the documentation <a href="https://libreevent.janishutz.com/docs/admin-panel/settings/admin-accounts#permissions" target="_blank">here</a>.</p> -->
<!-- <p style="margin-top: 0;">Usually, the permissions automatically set by the system on account creation should be appropriate. (TIP: Right click for more options)</p> -->
<div v-if="Object.keys( adminAccounts ).length > 0" class="account-wrapper">
<div v-for="account in adminAccounts" class="account" @click="showPasswordSettings( account.email );" title="Edit settings of this account (right click for more options)" @contextmenu="( e ) => { e.preventDefault(); openRightClickMenu( account.email, e ); }">
<div class="location-name">
<h3>{{ account.username }}</h3>
<p>{{ account.email }}</p>
</div>
</div>
</div>
<div v-else class="account-wrapper">
<p>No additional admin accounts configured yet.</p>
<button @click="createAccount()">Create new account</button>
</div>
</div>
<rightClickMenu ref="rclk" @command="( command ) => { executeCommand( command ) }"></rightClickMenu>
<popups ref="popup" size="big" @data="( data ) => { handlePopupReturns( data ); }"></popups>
<notifications ref="notification" location="topright" size="bigger"></notifications>
</div>
</template>
<script>
import settings from '@/components/settings/settings.vue';
import popups from '@/components/notifications/popups.vue';
import rightClickMenu from '@/components/settings/rightClickMenu.vue';
import notifications from '@/components/notifications/notifications.vue';
export default {
name: 'adminSettingsView',
components: {
settings,
popups,
rightClickMenu,
notifications,
},
data () {
return {
adminAccounts: { 'janis': { 'username': 'janis', 'email': 'info@janishutz.com', 'permissions': [ ] }, 'admin': { 'username': 'admin', 'email': 'development@janishutz.com', 'permissions': [ ] } },
currentlyOpenMenu: '',
currentPopup: '',
settings: {
'2fa': {
'display': 'Require Two-Factor-Authentication of user',
'id': '2fa',
'tooltip': 'Control whether or not users are required to use Two-Factor-Authentication. Defaults to "User can decide", which is recommended',
'value': 'enforce',
'type': 'select',
'restrictions': {
'enforce': {
'displayName': 'Always require',
'value': 'enforce'
},
'allow': {
'displayName': 'User can decide',
'value': 'allow'
},
'disable': {
'displayName': 'Disable',
'value': 'disable'
},
}
},
'currency': {
'display': 'Currency',
'id': 'currency',
'tooltip': 'Specify a currency in which the prices are displayed to the customer. Defaults to USD. Please use valid currency codes.',
'value': 'USD',
'type': 'text',
},
'ticketTimeout': {
'display': 'Ticket Timeout (s)',
'id': 'ticketTimeout',
'tooltip': 'Specify how long the user has to be inactive for their order to be canceled. Time is to be specified in seconds',
'value': 900,
'type': 'number',
'restrictions': {
'min': 0,
'max': 10000,
}
},
'paymentGateway': {
'display': 'Select the payment gateway to use',
'id': 'paymentGateway',
'tooltip': 'With this setting you may change which payment gateway you want to use. You will need to provide details below! If you are not sure what this setting means, please click the link below.',
'value': 'stripe',
'type': 'select',
'restrictions': {
'payrexx': {
'displayName': 'Payrexx',
'value': 'payrexx'
},
'stripe': {
'displayName': 'Stripe',
'value': 'stripe'
},
}
},
// 'addressRequired': {
// 'display': 'Require user to provide address?',
// 'id': 'addressRequired',
// 'tooltip':'With this toggle you may specify whether or not a user has to provide their address when purchasing something. (Keep GDPR in mind when processing data!)',
// 'value': false,
// 'type': 'toggle',
// },
// 'phoneNumberRequired': {
// 'display': 'Require user to provide phone number?',
// 'id': 'phoneNumberRequired',
// 'tooltip':'With this toggle you may specify whether or not a user has to provide their phone number when purchasing something. (Keep GDPR in mind when processing data!)',
// 'value': false,
// 'type': 'toggle',
// },
// 'dobRequired': {
// 'display': 'Require user to provide their birth date?',
// 'id': 'dobRequired',
// 'tooltip':'With this toggle you may specify whether or not a user has to provide their date of birth when purchasing something. (Keep GDPR in mind when processing data!)',
// 'value': false,
// 'type': 'toggle',
// },
}
};
},
methods: {
showAccountSettings ( account ) {
this.currentPopup = 'permissions';
this.$refs.popup.openPopup( 'Edit user permissions for ' + this.adminAccounts[ account ][ 'username' ], {
'pagesSettings': {
'display': 'Modify pages',
'id': 'pagesSettings',
'tooltip': 'Change this setting to allow or disallow the selected user to access and change any settings of pages like the start page.',
'value': false,
'type': 'toggle',
},
'locationsSettings': {
'display': 'Location settings and seat plans',
'id': 'locationsSettings',
'tooltip': 'Change this setting to allow or disallow the selected user to modify, delete or create locations with their corresponding seat plans.',
'value': false,
'type': 'toggle',
},
'plugins': {
'display': 'Plugin management',
'id': 'plugins',
'tooltip': 'Change this setting to allow or disallow the selected user to install or uninstall plugins. Some plugins might allow you to set extra permissions inside of their settings panels',
'value': false,
'type': 'toggle',
},
'events': {
'display': 'Event management',
'id': 'events',
'tooltip': 'Change this setting to allow or disallow the selected user to install or uninstall plugins. Some plugins might allow you to set extra permissions inside of their settings panels',
'value': false,
'type': 'toggle',
},
}
, 'settings' );
},
showPasswordSettings ( account ) {
this.currentlyOpenMenu = account;
this.currentPopup = 'account';
this.$refs.popup.openPopup( 'Edit user settings for ' + this.adminAccounts[ account ][ 'username' ], {
'username': {
'display': 'Username',
'id': 'username',
'tooltip': 'Change the username for this user.',
'value': this.adminAccounts[ account ][ 'username' ],
'type': 'text',
},
'pass': {
'display': 'Password',
'id': 'pass',
'tooltip': 'Change the password for this user.',
'value': '',
'type': 'password',
},
}, 'settings' );
},
showPaymentSettings () {
this.currentPopup = 'payments';
fetch( '/admin/getAPI/getPaymentGatewaySettings' ).then( res => {
if ( res.status === 200 ) {
res.json().then( json => {
this.$refs.popup.openPopup( 'Payment gateway settings for ' + json.gateway, json.data, 'settings' );
} );
} else if ( res.status === 500 ) {
this.$refs.notification.createNotification( 'This payment gateway does not appear to have settings', 10, 'error', 'normal' );
}
} );
},
createAccount() {
this.currentPopup = 'createAccount';
this.$refs.popup.openPopup( 'Create new admin user', {
// 'role': {
// 'display': 'User role',
// 'id': 'role',
// 'tooltip':'With this setting you can choose one of the preset permissions for users. Account management is only allowed for the root user.',
// 'value': 'eventManager',
// 'type': 'select',
// 'restrictions': {
// 'fullAccess': {
// 'value': 'fullAccess',
// 'displayName': 'Full Access'
// },
// 'eventManager': {
// 'value': 'eventManager',
// 'displayName': 'Event Manager'
// },
// 'entryControl': {
// 'value': 'entryControl',
// 'displayName': 'Entry Control'
// }
// }
// },
'username': {
'display': 'Username',
'id': 'username',
'tooltip': 'Add a username for this user',
'value': '',
'type': 'text',
},
'email': {
'display': 'Email',
'id': 'email',
'tooltip': 'Add an email-address for this user',
'value': '',
'type': 'text',
},
'pass': {
'display': 'Password',
'id': 'pass',
'tooltip': 'Create a password for this user.',
'value': '',
'type': 'password',
},
'two_fa': {
'display': 'Two Factor Authentication',
'id': 'two_fa',
'tooltip': 'With this setting you may change the 2FA Authentication should work for this user. Enhanced requires the user to enter a code, simple solely to click a link',
'value': 'enhanced',
'type': 'select',
'restrictions': {
'enhanced': {
'value': 'enhanced',
'displayName': 'Enhanced'
},
'eventManager': {
'value': 'simple',
'displayName': 'Simple'
},
'disabled': {
'value': 'disabled',
'displayName': 'Disabled'
}
}
},
}
, 'settings' );
},
executeCommand( command ) {
// if ( command === 'openPermissions' ) {
// this.currentPopup = 'account';
// this.showAccountSettings( this.currentlyOpenMenu );
// } else
if ( command === 'deleteUser' ) {
this.currentPopup = 'deleteUser';
this.$refs.popup.openPopup( 'Do you really want to delete the user ' + this.currentlyOpenMenu + '?', {}, 'confirm' );
} else if ( command === 'updatePassword' ) {
this.currentPopup = 'deleteUser';
this.showPasswordSettings( this.currentlyOpenMenu );
}
},
handlePopupReturns( data ) {
if ( data.status === 'cancel' ) {
return;
} else if ( data.status === 'settings' ) {
if ( this.currentPopup === 'account' ) {
if ( data.data.username != '' ) {
let updatedData = data.data;
if ( updatedData.pass == '' ) {
delete updatedData[ 'pass' ];
}
updatedData[ 'email' ] = this.currentlyOpenMenu;
let fetchOptions = {
method: 'post',
body: JSON.stringify( updatedData ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( '/admin/API/updateAdminAccount', fetchOptions ).then( res => {
if ( res.status === 200 ) {
this.$refs.notification.createNotification( 'Updated settings for admin account successfully', 5, 'ok', 'normal' );
this.loadAdminAccounts();
}
} );
}
} else if ( this.currentPopup === 'payments' ) {
for ( let setting in data.data ) {
if ( !data.data[ setting ] ) {
this.$refs.notification.createNotification( 'Settings for the payment gateway are missing!', 10, 'error', 'normal' );
this.showPaymentSettings();
return;
}
}
let fetchOptions = {
method: 'post',
body: JSON.stringify( data.data ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( '/admin/API/updatePaymentGatewaySettings', fetchOptions ).then( res => {
if ( res.status === 200 ) {
this.$refs.notification.createNotification( 'Payment gateway settings saved!', 5, 'ok', 'normal' );
}
} );
} else if ( this.currentPopup === 'createAccount' ) {
let fetchOptions = {
method: 'post',
body: JSON.stringify( data.data ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( '/admin/API/createAdminAccount', fetchOptions ).then( res => {
if ( res.status === 200 ) {
this.$refs.notification.createNotification( 'Created new admin account successfully', 5, 'ok', 'normal' );
this.loadAdminAccounts();
}
} );
}
} else if ( data.status === 'ok' ) {
if ( this.currentPopup === 'deleteUser' ) {
let fetchOptions = {
method: 'post',
body: JSON.stringify( { 'email': this.currentlyOpenMenu } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( '/admin/API/deleteAdminAccount', fetchOptions ).then( res => {
if ( res.status === 200 ) {
this.$refs.notification.createNotification( 'Admin account deleted successfully', 5, 'ok', 'normal' );
this.loadAdminAccounts();
}
} );
}
} else {
console.log( 'hi' );
}
},
openRightClickMenu( id, event ) {
this.$refs.rclk.openRightClickMenu( event, {
// 'permissions': { 'command': 'openPermissions', 'symbol': 'edit', 'display': 'Edit permissions' },
'password': { 'command': 'updatePassword', 'symbol': 'password', 'display': 'Edit account settings' },
'delete': { 'command': 'deleteUser', 'symbol': 'delete', 'display': 'Delete User' } } );
this.currentlyOpenMenu = id;
},
loadData() {
fetch( '/admin/getAPI/getSettings' ).then( res => {
if ( res.status === 200 ) {
res.json().then( json => {
this.settings[ '2fa' ].value = json.twoFA;
this.settings.currency.value = json.currency;
this.settings.paymentGateway.value = json.payments;
this.settings.ticketTimeout.value = json.ticketTimeout;
} );
}
} );
},
loadAdminAccounts () {
fetch( '/admin/getAPI/getAdminAccounts' ).then( res => {
if ( res.status === 200 ) {
res.json().then( json => {
if ( json.status === 'ok' ) {
this.adminAccounts = {};
for ( let account in json.data ) {
this.adminAccounts[ json.data[ account ][ 'email' ] ] = json.data[ account ];
}
} else {
this.adminAccounts = {};
}
} );
}
} );
},
save() {
let fetchOptions = {
method: 'post',
body: JSON.stringify( {
'twoFA': this.settings[ '2fa' ].value,
'currency': this.settings.currency.value,
'payments': this.settings.paymentGateway.value,
'ticketTimeout': this.settings.ticketTimeout.value,
} ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/admin/API/updateSettings', fetchOptions ).then( res => {
if ( res.status === 200 ) {
this.$refs.notification.createNotification( 'Saved settings successfully. Restart libreevent to apply changes', 20, 'ok', 'normal' );
this.loadData();
}
} );
}
},
created () {
this.loadData();
this.loadAdminAccounts();
}
};
</script>
<style scoped>
.account-wrapper {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.gateway-settings {
width: 70%;
text-align: justify;
margin-left: 15%;
}
.admin-settings {
text-align: justify;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.account {
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
color: var( --primary-color );
border-color: var( --primary-color );
border-width: 1px;
height: fit-content;
border-style: solid;
padding: 10px;
transition: 0.4s;
cursor: pointer;
width: 60%;
margin-bottom: 5px;
}
.account:hover {
background-color: var( --hover-color );
transition: 0.4s;
}
.location-name {
margin-right: auto;
max-width: 35%;
}
</style>

View File

@@ -1,139 +0,0 @@
<template>
<div id="twoFA">
<h1>Two-Factor Authentication</h1>
<p>We have sent you an email containing a link for Authentication.</p>
<div class="code-container" v-if="code[ 1 ] != ''">
<p>Open the link in the email and enter this code:</p>
<div class="code">
<div class="code-sub" id="code-part1">{{ code[1] }}</div>
<div class="code-sub" id="code-part2">{{ code[2] }}</div>
</div>
</div>
<notifications ref="notification" location="bottomright" size="bigger"></notifications>
</div>
</template>
<script>
import notifications from '@/components/notifications/notifications.vue';
import { useUserStore } from '@/stores/userStore';
import { mapStores } from 'pinia';
export default {
name: 'twoFAAdmin',
components: {
notifications
},
data () {
return {
code: { '1': '', '2': '' }
};
},
computed: {
...mapStores( useUserStore ),
},
created () {
if ( this.userStore.getAdminTwoFACompliant ) {
if ( window.EventSource ) {
setTimeout( () => {
let startNotification = this.$refs.notification.createNotification( 'Connecting to status service', 20, 'progress', 'normal' );
let source = new EventSource( localStorage.getItem( 'url' ) + '/admin/2fa/check', { withCredentials: true } );
let self = this;
source.onmessage = ( e ) => {
if ( e.data === 'authenticated' ) {
self.userStore.setAdminAuth( true );
self.$router.push( '/admin' );
console.log( e.data );
}
};
source.onopen = () => {
self.$refs.notification.createNotification( 'Connected to status service', 5, 'ok', 'normal' );
self.$refs.notification.cancelNotification( startNotification );
};
source.addEventListener( 'error', function( e ) {
if ( e.eventPhase == EventSource.CLOSED ) source.close();
if ( e.target.readyState == EventSource.CLOSED ) {
console.log( e );
self.$refs.notification.cancelNotification( startNotification );
self.$refs.notification.createNotification( 'Could not connect to status service', 5, 'error', 'normal' );
}
}, false );
}, 300 );
} else {
setTimeout( () => {
this.$refs.notification.createNotification( 'Unsupported browser detected. Redirection might take longer to occur!', 20, 'warning', 'normal' );
}, 300 );
// ping server every 5s to check if logged in
this.serverPing = setInterval( () => {
fetch( '/admin/2fa/ping' ).then( res => {
if ( res.status === 200 ) {
res.json().then( data => {
if ( data ) {
if ( data.status === 'ok' ) {
this.userStore.setUserAuth( true );
this.$router.push( sessionStorage.getItem( 'redirect' ) ?? '/account' );
}
}
} );
} else {
console.error( 'Request failed' );
this.$refs.notification.createNotification( 'We are sorry, but an error occurred. You will not be redirected automatically', 300, 'error', 'normal' );
}
} ).catch( error => {
console.error( error );
this.$refs.notification.createNotification( 'We are sorry, but an error occurred. You will not be redirected automatically', 300, 'error', 'normal' );
} );
}, 5000 );
}
let code = sessionStorage.getItem( '2faCode' ) ? sessionStorage.getItem( '2faCode' ) : '';
this.code = { '1': code.slice( 0, 3 ), '2': code.substring( 3 ) };
} else {
if ( this.userStore.getAdminAuthenticated ) {
this.$router.push( '/admin' );
} else {
this.$router.push( '/admin/login' );
}
}
},
};
</script>
<style scoped>
#twoFA, .code-container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.code-container {
width: fit-content;
padding: 5% 8%;
border: var( --primary-color ) solid 2px;
border-radius: 10px;
margin-top: 3%;
background-color: var( --popup-color );
}
.code {
background-color: var( --hover-color );
padding: 7% 10%;
margin-bottom: 0;
width: fit-content;
border-radius: 10px;
font-size: 200%;
font-family: monospace;
display: block;
}
.code-sub {
display: inline-block;
}
#code-part2 {
margin-left: 7px;
}
</style>

View File

@@ -1,18 +0,0 @@
<!--
* libreevent - AnalyticsView.vue
*
* Created by Janis Hutz 06/28/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h2>Event analytics</h2>
<p>Get insights into tickets sold, people checked in, available tickets, revenue, etc, in real time!</p>
<p>Coming soon! (in the next major update)</p>
<!-- TODO: FUTURE Add toggle for real-time update (gets live updates from sse) -->
<!-- Use chart.js for visualisations -->
</div>
</template>

View File

@@ -1,47 +0,0 @@
<!--
* libreevent - EditorView.vue
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h2>Seat Plan Editor ({{ location }})</h2>
<window />
</div>
</template>
<script>
import window from '@/components/seatplan/editor/window.vue';
export default {
data () {
return {
location: '',
};
},
components: {
window,
},
methods: {
checkLocationSelected () {
if ( !sessionStorage.getItem( 'locationID' ) ) {
this.$router.push( '/admin/locations' );
}
this.location = sessionStorage.getItem( 'locationID' );
}
},
created() {
this.checkLocationSelected();
}
};
</script>
<style>
nav {
display: none;
}
</style>

View File

@@ -1,544 +0,0 @@
<!--
* libreevent - TicketsDetailsView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="details">
<h2>{{ event.name }}</h2>
<button @click="save( 'draft' )">Save as draft</button>
<div class="category-wrapper">
<h3>Event Details</h3>
<table class="category">
<tr>
<td>Event name</td>
<td><input type="text" v-model="event.name"></td>
</tr>
<tr>
<td>Event location</td>
<td v-if="Object.keys( locations ).length > 0">
<select v-model="event.location" class="small-text" @change="handleLocationChange()">
<option v-for="location in locations" :value="location.locationID">{{ location.locationID }} ({{ location.name }})</option>
</select>
</td>
<td v-else>No locations configured yet. Configure one <router-link to="/admin/locations">here</router-link></td>
</tr>
<tr>
<td>Event date and time</td>
<td><input v-model="event.date" class="small-text" type="date"><input v-model="event.time" class="small-text" type="time"></td>
</tr>
<tr>
<td>Ticket editor</td>
<router-link to="/admin/ticketEditor">Edit ticket layout</router-link>
</tr>
</table>
<h4>Event description</h4>
<textarea v-model="event.description" class="big-text" cols="70" rows="3" placeholder="Event description..."></textarea>
</div>
<div class="ticket-settings">
<h3>Age Groups</h3>
<button @click="addNew( 'ageGroup' )">Add another age group</button>
<p>With these settings you can manage and create different age categories which have to be set for every ticket.</p>
<div class="category-wrapper">
<table class="category" v-for="ageGroup in event.ageGroups">
{{ ageGroup.name }} <span class="material-symbols-outlined deleteButton" @click="deleteObject( 'ageGroup', ageGroup.id )" title="Delete age group">delete</span>
<tr class="category-details">
<td>
<div class="category-details">Group name: </div>
</td>
<td>
<input type="text" v-model="ageGroup.name">
</td>
</tr>
<tr>
<td>
<div class="category-details">Age (Leave empty for no age info): </div>
</td>
<td>
<input type="text" v-model="ageGroup.age">
</td>
</tr>
</table>
</div>
</div>
<div class="ticket-settings">
<h3>Categories</h3>
<button @click="addNew( 'category' )">Add another category</button>
<p>The foreground and background colours of the seats are used to show the customer to which category the seats belong.</p>
<div class="category-wrapper">
<table class="category" v-for="category in event.categories">
{{ category.name }} <span class="material-symbols-outlined deleteButton" @click="deleteObject( 'category', category.id )" title="Delete category">delete</span>
<tr v-for="ageGroup in event.ageGroups">
<td>
<div class="category-details">{{ ageGroup.name }}<div style="display: inline;" v-if="ageGroup.age"> ({{ ageGroup.age }})</div>:</div>
</td>
<td>
{{ currency }} <input type="number" v-model="category.price[ ageGroup.id ]">
</td>
</tr>
<tr>
<td><div class="category-details">Colour:</div></td>
<td>
<input type="text" data-coloris v-model="category.fg" onkeydown="return false;">
</td>
</tr>
<tr v-if="!hasSeatPlan">
<td><div class="category-details">Total tickets for this category</div></td>
<td>
<input type="number" v-model="category.ticketCount">
</td>
</tr>
</table>
</div>
</div>
<div>
<h3>Assets</h3>
<p>Here you can change all the marketing images for your event. All assets have to be jpg images.</p>
<div style="display: flex;">
<picture-input
ref="logo"
:width="200"
:removable="false"
removeButtonClass="ui red button"
:height="200"
accept="image/jpeg"
buttonClass="ui button primary"
:customStrings="{
upload: '<h1>Upload your image!</h1>',
drag: 'Drag and drop your event logo here'
}">
</picture-input>
<picture-input
ref="banner"
:width="355"
:removable="false"
removeButtonClass="ui red button"
:height="200"
accept="image/jpeg"
buttonClass="ui button primary"
:customStrings="{
upload: '<h1>Upload your image!</h1>',
drag: 'Drag and drop your event banner here'
}">
</picture-input>
</div>
<button @click="saveImages()">Upload</button>
</div>
<div class="special-settings">
<h3>General Settings</h3>
<settings v-model:settings="specialSettings"></settings>
</div>
<button @click="save( 'draft' )">Save as draft</button>
<div>
<h3>Danger Zone</h3>
<button @click="dangerZone( 'deploy' )">Go Live</button>
<button @click="dangerZone( 'undeploy' )" v-if="hasLiveVersion">Unpublish event</button>
<button @click="dangerZone( 'delete' )">Delete Event</button>
<br><br><br>
</div>
<!-- <div>
<p>Please read the documentation of this section if you want to use the requirements. It requires specific syntax to work. See <a href="https://libreevent.janishutz.com/docs/admin-panel/events#special-requirements" target="_blank">here</a> for more information</p>
</div> -->
<notifications ref="notification" location="topright"></notifications>
<popups ref="popups" size="normal" @data="( data ) => { handlePopup( data ) }"></popups>
</div>
</template>
<style scoped>
.details {
flex-grow: 1;
}
.ticket-settings {
width: 100%;
}
.category-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
flex-direction: column;
}
.category {
width: 50%;
text-align: justify;
}
.category-details {
margin-left: 7%;;
}
.deleteButton {
cursor: pointer;
font-size: 110%;
margin: 0;
}
</style>
<script>
import settings from '@/components/settings/settings.vue';
import notifications from '@/components/notifications/notifications.vue';
import popups from '@/components/notifications/popups.vue';
import PictureInput from 'vue-picture-input';
export default {
name: 'TicketsDetailsView',
components: {
settings,
notifications,
PictureInput,
popups,
},
data() {
return {
locations: {},
event: { 'name': 'Unnamed event', 'description': '', 'location': '', 'date': '', 'categories': {}, 'ageGroups': { '1': { 'id': 1, 'name': 'Child', 'age': '0 - 15.99' }, '2': { 'id': 2, 'name': 'Adult' } }, 'maxTickets': 2, 'eventID': 'untitled' },
specialSettings: {
// 'guest-purchase': {
// 'display': 'Enable guest purchase',
// 'id': 'guest-purchase',
// 'tooltip':'Allowing guest purchase means that a user does not have to create an account in order for them to be able to make a purchase. Default: On',
// 'value': true,
// 'type': 'toggle'
// },
// 'overbooking': {
// 'display': 'Enable overbooking of event',
// 'id': 'overbooking',
// 'tooltip':'Allow more ticket reservations than you have tickets available. Currently only available for events without seatplans. Default: Off',
// 'value': false,
// 'type': 'toggle'
// },
'maxTickets': {
'display': 'Maximum ticket count per account',
'id': 'maxTickets',
'tooltip': 'With this setting you can control how many tickets a person can buy. Defaults to 0, which means do not limit.',
'value': 0,
'type': 'number',
'restrictions': {
'min': 0,
'max': 100,
}
},
// 'requiredParameter': {
// 'display': 'Special requirements',
// 'id': 'requiredParameter',
// 'tooltip':'Set this parameter to require the user to provide a certain email domain, a special number or special string of characters. Defaults to None',
// 'value': 'none',
// 'type': 'select',
// 'restrictions': {
// 'none': {
// 'displayName':'None',
// 'value': 'none'
// },
// 'email': {
// 'displayName':'Email domain',
// 'value': 'email'
// },
// 'numbers': {
// 'displayName':'Number sequence',
// 'value': 'numbers'
// },
// 'string': {
// 'displayName':'Text sequence',
// 'value': 'string'
// },
// }
// },
// 'requiredParameterValue': {
// 'display': 'Special requirements values ',
// 'id': 'requiredParameterValue',
// 'tooltip':'Put a filter here, corresponding to your selection above. Please read the documentation on our website. See link below!',
// 'value': '',
// 'type': 'text',
// },
},
command: '',
currentLocation: '',
toDelete: '',
currency: 'USD',
hasLiveVersion: false,
hasSeatPlan: true,
totalSeats: 0,
};
},
created () {
this.loadData();
},
methods: {
loadData () {
if ( !sessionStorage.getItem( 'selectedTicket' ) ) {
this.$router.push( '/admin/events' );
}
fetch( localStorage.getItem( 'url' ) + '/admin/getAPI/getCurrency' ).then( res => {
res.text().then( currency => {
this.currency = currency;
} );
} );
fetch( localStorage.getItem( 'url' ) + '/admin/getAPI/getEventStatus' ).then( res => {
res.text().then( status => {
if ( status === 'true' ) {
this.hasLiveVersion = true;
} else {
this.hasLiveVersion = false;
}
} );
} );
this.eventID = sessionStorage.getItem( 'selectedTicket' );
fetch( localStorage.getItem( 'url' ) + '/admin/getAPI/getLocations' ).then( res => {
res.json().then( data => {
this.locations = data;
fetch( localStorage.getItem( 'url' ) + '/admin/getAPI/getEvent?event=' + this.eventID ).then( res => {
if ( res.status === 200 ) {
res.json().then( data => {
this.event = data;
this.currentLocation = this.event.location;
const dt = this.event.date.split( 'T' );
this.event.date = dt[ 0 ];
this.event.time = dt[ 1 ].slice( 0, dt[ 1 ].length - 1 );
this.hasSeatPlan = this.locations[ this.event.location ] ? ( this.locations[ this.event.location ][ 'seatplan-enabled' ] ?? false ) : false;
} ).catch( error => {
console.error( error );
} );
} else if ( res.status === 404 ) {
this.$router.push( '/admin/events' );
}
} );
} ).catch( error => {
console.error( error );
} );
} );
},
saveImages() {
if ( this.$refs.logo.file && this.$refs.banner.file ) {
let fd = new FormData();
console.log( this.$refs.logo.file );
fd.append( 'image', this.$refs.logo.file );
fd.append( 'image', this.$refs.banner.file );
fd.append( 'logo', this.$refs.logo.file.name );
let fetchOptions = {
method: 'post',
body: fd,
};
fetch( localStorage.getItem( 'url' ) + '/admin/events/uploadImages?event=' + sessionStorage.getItem( 'selectedTicket' ) + '&image=' + 'logo', fetchOptions ).then( res => {
if ( res.status === 200 ) {
this.$refs.notification.createNotification( 'Images saved successfully!', 5, 'ok', 'normal' );
}
} ).catch( err => {
console.error( err );
} );
} else {
this.$refs.notification.createNotification( 'No image selected!', 5, 'error', 'normal' );
}
},
save ( action ) {
if ( Object.keys( this.event.ageGroups ).length > 0 && Object.keys( this.event.categories ).length > 0 ) {
for ( let ageGroup in this.event.ageGroups ) {
if ( this.event.ageGroups[ ageGroup ].name == '' ) {
this.$refs.popups.openPopup( 'One or more age groups are missing their names. Please ensure that all age groups have a name and try again!', {}, 'string' );
return;
}
}
let lowestPrice = 1000000;
let totalSeats = parseInt( this.locations[ this.event.location ].totalSeats ?? 0 );
for ( let category in this.event.categories ) {
for ( let price in this.event.categories[ category ].price ) {
if ( this.event.categories[ category ].price[ price ] < 0.5 || ( !this.event.categories[ category ].ticketCount && this.hasSeatPlan ) ) {
this.$refs.popups.openPopup( 'At least one of the prices for at least one of the categories is below the minimum of ' + this.currency + ' 0.5', {}, 'string' );
return;
}
if ( this.event.categories[ category ].price[ price ] < lowestPrice ) {
lowestPrice = this.event.categories[ category ].price[ price ];
}
}
totalSeats += parseInt( this.event.categories[ category ].ticketCount ?? 0 );
}
this.event[ 'startingPrice' ] = lowestPrice;
this.event[ 'currency' ] = this.currency;
this.event[ 'locationName' ] = this.locations[ this.event.location ].name;
this.event[ 'hasSeatplan' ] = this.hasSeatPlan;
this.event[ 'totalSeats' ] = totalSeats;
const fullDate = new Date( this.event.date + 'T' + this.event.time +'Z' );
this.event.date = fullDate.toISOString();
if ( !this.event.maxTickets ) {
this.event.maxTickets = this.totalSeats;
}
this.event.maxTickets = this.specialSettings[ 'maxTickets' ].value;
let url = localStorage.getItem( 'url' ) + '/admin/api/saveEvent';
if ( action === 'deploy' ) {
url = localStorage.getItem( 'url' ) + '/admin/api/deployEvent';
}
const options = {
method: 'post',
body: JSON.stringify( { 'event': this.event.eventID, 'eventData': this.event } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( url, options ).then( res => {
if ( res.status === 200 ) {
if ( action === 'deploy' ) {
this.$refs.notification.createNotification( 'Your event has been published successfully.', 5, 'ok', 'normal' );
fetch( '/getAPI/reloadData' ).catch( () => {} );
this.hasLiveVersion = true;
} else {
this.$refs.notification.createNotification( 'Saved as draft successfully!', 5, 'ok', 'normal' );
}
this.loadData();
}
} );
} else {
this.$refs.popups.openPopup( 'Please ensure that you have at least one age group and one category defined!', {}, 'string' );
}
},
addNew( type ) {
if ( type === 'ageGroup' ) {
this.$refs.popups.openPopup( 'Choose a name for the age group', {}, 'text' );
this.command = 'addAgeGroup';
} else if ( type === 'category' ) {
this.$refs.popups.openPopup( 'Choose a name for the new category', {}, 'number', Object.keys( this.event.categories ).length + 1 );
this.command = 'addCategory';
}
},
deleteObject( type, data ) {
if ( type === 'ageGroup' ) {
this.$refs.popups.openPopup( 'Do you really want to delete this age group?', {}, 'confirm' );
this.command = 'deleteAgeGroup';
this.toDelete = data;
} else if ( type === 'category' ) {
this.$refs.popups.openPopup( 'Do you really want to delete this category', {}, 'confirm' );
this.command = 'deleteCategory';
this.toDelete = data;
}
},
handleLocationChange() {
if ( Object.keys( this.event.categories ).length > 1 && this.locations[ this.event.location ][ 'seatplan-enabled' ] ) {
this.command = 'locationChange';
this.$refs.popups.openPopup( 'You have edited the categories of this location. Changing the location now leads to data loss.', {}, 'confirm' );
} else {
this.command = 'locationChange';
this.handlePopup( { 'status': 'ok' } );
}
},
handlePopup( data ) {
if ( data.status === 'ok' ) {
if ( this.command === 'addCategory' ) {
this.command = '';
if ( !this.event.categories[ data.data ] ) {
this.event.categories[ data.data ] = { 'price': {}, 'bg': '#FFFFFF', 'fg': '#000000', 'name': 'Category ' + data.data, 'id': data.data, 'ticketCount': 1 };
for ( let ageGroup in this.event.ageGroups ) {
this.event.categories[ data.data ][ 'price' ][ ageGroup ] = 0;
}
} else {
this.$refs.popups.openPopup( 'That category already exists!', {}, 'string' );
}
} else if ( this.command === 'addAgeGroup' ) {
this.command = '';
for ( let ageGroup in this.event.ageGroups ) {
if ( this.event.ageGroups[ ageGroup ].name === data.data ) {
this.$refs.popups.openPopup( 'That age group exists already', {}, 'string' );
return;
}
}
this.event.ageGroups[ Object.keys( this.event.categories ).length + 1 ] = { 'id': Object.keys( this.event.categories ).length + 1, 'name': data.data };
for ( let ageGroup in this.event.ageGroups ) {
for ( let category in this.event.categories ) {
if ( !this.event.categories[ category ][ 'price' ][ ageGroup ] ) {
this.event.categories[ category ][ 'price' ][ ageGroup ] = 0;
}
}
}
} else if ( this.command === 'deleteCategory' ) {
delete this.event.categories[ this.toDelete ];
} else if ( this.command === 'deleteAgeGroup' ) {
delete this.event.ageGroups[ this.toDelete ];
} else if ( this.command === 'locationChange' ) {
this.currentLocation = this.event.location;
if ( this.locations[ this.event.location ][ 'seatplan-enabled' ] ) {
fetch( '/admin/getAPI/getSeatplan?location=' + this.event.location ).then( res => {
if ( res.status === 200 ) {
res.json().then( json => {
this.hasSeatPlan = this.locations[ this.event.location ][ 'seatplan-enabled' ] ?? false;
this.event.categories = {};
this.totalSeats = json.seatInfo.count;
for ( let element in json.data ) {
if ( json.data[ element ][ 'type' ] === 'seat' || json.data[ element ][ 'type' ] === 'stand' ) {
this.event.categories[ json.data[ element ][ 'category' ] ] = { 'price': {}, 'bg': '#FFFFFF', 'fg': '#000000', 'name': 'Category ' + json.data[ element ][ 'category' ], 'id': json.data[ element ][ 'category' ], 'ticketCount': 1 };
for ( let ageGroup in this.event.ageGroups ) {
this.event.categories[ json.data[ element ][ 'category' ] ][ 'price' ][ ageGroup ] = 0;
}
}
}
} );
}
} );
}
} else if ( this.command === 'deployEvent' ) {
this.save( 'deploy' );
} else if ( this.command === 'undeployEvent' ) {
const options = {
method: 'post',
body: JSON.stringify( { 'event': sessionStorage.getItem( this.event.eventID ) } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( '/admin/api/undeployEvent', options ).then( res => {
if ( res.status === 200 ) {
this.hasLiveVersion = false;
this.$refs.notification.createNotification( 'Your event is no longer publicly visible!', 5, 'ok', 'normal' );
} else {
this.$refs.notification.createNotification( 'There was an error hiding your event', 5, 'error', 'normal' );
}
} );
} else if ( this.command === 'deleteEvent' ) {
const options = {
method: 'post',
body: JSON.stringify( { 'event': sessionStorage.getItem( this.event.eventID ) } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( '/admin/api/deleteEvent', options ).then( res => {
if ( res.status === 200 ) {
this.$refs.notification.createNotification( 'Your event has been deleted successfully!', 5, 'ok', 'normal' );
setTimeout( () => {
this.$router.push( '/admin/events' );
}, 5000 );
} else {
this.$refs.notification.createNotification( 'There was an error deleting your event', 5, 'error', 'normal' );
}
} );
}
} else if ( data.status === 'cancel' ) {
if ( this.command === 'locationChange' ) {
this.event.location = this.currentLocation;
}
}
},
dangerZone( action ) {
if ( action === 'deploy' ) {
this.$refs.popups.openPopup( 'Do you really want to go live with this event?', {}, 'confirm' );
this.command = 'deployEvent';
} else if ( action === 'undeploy' ) {
this.$refs.popups.openPopup( 'Do you really want to remove this event from the event listings?', {}, 'confirm' );
this.command = 'undeployEvent';
} else if ( action === 'delete' ) {
this.$refs.popups.openPopup( 'Do you really want to delete this event? This action cannot be undone', {}, 'confirm' );
this.command = 'deleteEvent';
}
}
}
};
</script>

File diff suppressed because one or more lines are too long

View File

@@ -1,29 +0,0 @@
<!--
* libreevent - StartPageEditorView.vue
*
* Created by Janis Hutz 06/24/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h2>Page editor</h2>
</div>
</template>
<script>
export default {
data () {
return {
formData: {}
};
},
methods: {
setup () {
}
}
};
</script>

View File

@@ -1,249 +0,0 @@
<!--
* libreevent - CartView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="cart">
<h1>Cart</h1>
<div v-if="Object.keys( cart ).length > 0" class="cart-list">
<h3>Your tickets</h3>
<div v-for="event in cart">
<h3>{{ event.displayName }}</h3>
<table class="tickets-table">
<tr v-for="ticket in event.tickets">
<td>
<h4 class="price"><div style="display: inline;" v-if="ticket.count">{{ ticket.count }}x</div> {{ ticket.displayName }}: </h4>
</td>
<td>
{{ backend.currency }} {{ ticket.price }} <span class="material-symbols-outlined deleteButton" @click="deleteTicket( ticket.id, event.eventID, ticket.comp )" title="Delete ticket">delete</span>
</td>
</tr>
</table>
</div>
<div class="tool-wrapper">
<h4>Total: {{ backend.currency }} {{ backend.total }}</h4>
<router-link to="/purchase" id="toCartButton">Purchase now</router-link>
</div>
</div>
<div v-else>
Cart is empty. Please add tickets <router-link to="/tickets">here</router-link>
<div class="empty-cart-wrapper">
<span class="material-symbols-outlined empty-cart">remove_shopping_cart</span>
</div>
</div>
<popups ref="popups" size="small" @data="data => { verifyTicketDelete( data.status ) }"></popups>
</div>
</template>
<style scoped>
#toCartButton {
text-decoration: none;
padding: 2%;
width: fit-content;
background-color: var( --accent-color );
color: var( --secondary-color );
transition: all 1s;
border-radius: 50px;
margin-top: 2%;
}
#toCartButton:hover {
background-color: var( --accent-background-hover );
border-radius: 10px;
}
.cart {
text-align: justify;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.empty-cart-wrapper {
width: 100%;
height: 70vh;
display: flex;
justify-content: center;
align-items: center;
}
.empty-cart {
display: block;
font-size: 20rem;
}
.cart-list {
width: 90%;
}
ul {
list-style: none;
text-align: justify;
}
.tool-wrapper {
display: flex;
width: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
}
.deleteButton {
cursor: pointer;
font-size: 110%;
margin: 0;
}
.tickets-table {
margin-left: 3%;
}
@media only screen and (min-width: 999px) {
.cart-list {
width: 50%;
}
}
</style>
<script>
import popups from '@/components/notifications/popups.vue';
export default {
data() {
return {
cart: {},
backend: { 'currency': 'CHF' },
ticketToDelete: {},
};
},
components: {
popups,
},
methods: {
calculateTotal () {
this.backend[ 'total' ] = 0;
for ( let event in this.cart ) {
for ( let ticket in this.cart[ event ][ 'tickets' ] ) {
this.backend[ 'total' ] += parseInt( this.cart[ event ][ 'tickets' ][ ticket ][ 'price' ] ) * parseInt( this.cart[ event ][ 'tickets' ][ ticket ][ 'count' ] ?? 1 );
}
}
},
deleteTicket ( ticketID, event, component ) {
this.ticketToDelete[ 'event' ] = event;
this.ticketToDelete[ 'id' ] = ticketID;
this.ticketToDelete[ 'component' ] = component;
this.$refs.popups.openPopup( 'Do you really want to delete this ticket?', {}, 'confirm' );
},
verifyTicketDelete ( status ) {
if ( status === 'ok' ) {
if ( Object.keys( this.cart[ this.ticketToDelete.event ][ 'tickets' ] ).length > 1 ) {
delete this.cart[ this.ticketToDelete.event ][ 'tickets' ][ this.ticketToDelete.id ];
} else {
delete this.cart[ this.ticketToDelete.event ];
}
const options = {
method: 'post',
body: JSON.stringify( { 'id': this.ticketToDelete[ 'id' ], 'eventID': this.ticketToDelete[ 'event' ], 'component': this.ticketToDelete[ 'component' ] } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/API/deselectTicket', options );
}
localStorage.setItem( 'cart', JSON.stringify( this.cart ) );
},
seatChecks ( event ) {
let self = this;
let allSeatsAvailable = true;
fetch( localStorage.getItem( 'url' ) + '/getAPI/getReservedSeats?event=' + event ).then( res => {
if ( res.status === 200 ) {
let unavailableSeats = {};
res.json().then( data => {
for ( let seat in data.reserved ) {
if ( data.reserved[ seat ] ) {
if ( !unavailableSeats[ data.reserved[ seat ].component ] ) {
unavailableSeats[ data.reserved[ seat ].component ] = {};
}
unavailableSeats[ data.reserved[ seat ].component ][ data.reserved[ seat ].id ] = 'nav';
}
}
for ( let seat in data.user ) {
if ( data.user[ seat ] ) {
if ( !unavailableSeats[ data.user[ seat ].component ] ) {
unavailableSeats[ data.user[ seat ].component ] = {};
}
unavailableSeats[ data.user[ seat ].component ][ data.user[ seat ].id ] = 'sel';
}
}
let tickets = {};
if ( this.cart[ event ] ) {
tickets = this.cart[ event ][ 'tickets' ];
}
if ( data.user ) {
for ( let element in tickets ) {
if ( !data.user[ element ] ) {
allSeatsAvailable = false;
if ( Object.keys( this.cart[ event ][ 'tickets' ] ).length > 1 ) {
delete this.cart[ event ][ 'tickets' ][ element ];
} else {
delete this.cart[ event ];
}
}
}
} else {
delete this.cart[ event ];
allSeatsAvailable = false;
}
this.unavailableSeats = unavailableSeats;
if ( !allSeatsAvailable ) {
setTimeout( () => {
self.$refs.popups.openPopup( 'We are sorry to tell you that since the last time the seat plan was refreshed, one or more of the seats you have selected has/have been taken.', {}, 'string' );
}, 500 );
localStorage.setItem( 'cart', JSON.stringify( this.cart ) );
}
} );
} else {
console.error( 'unable to load' );
}
} );
},
},
created () {
window.addEventListener( 'visibilitychange', () => {
this.cart = localStorage.getItem( 'cart' ) ? JSON.parse( localStorage.getItem( 'cart' ) ): {};
this.calculateTotal();
}, 1 );
this.cart = localStorage.getItem( 'cart' ) ? JSON.parse( localStorage.getItem( 'cart' ) ): {};
this.calculateTotal();
for ( let event in this.cart ) {
this.seatChecks( event );
}
}
};
</script>
<style>
nav {
display: initial;
}
.price {
margin: 0;
padding: 0;
}
</style>

View File

@@ -1,28 +0,0 @@
<!--
* libreevent - GuestPurchaseView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h1>Guest purchase</h1>
</div>
</template>
<script>
export default {
methods: {
},
data() {
return {};
},
created() {
}
};
</script>

View File

@@ -1,159 +0,0 @@
<!--
* libreevent - OrderView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="order">
<h1>Order tickets</h1>
<div class="order-app" v-if="Object.keys( orderedEvents ).length">
<div v-for="event in orderedEvents" style="width: 80%;">
<router-link to="/tickets/details" class="ticket" @click="setActiveTicket( event.eventID );">
<img :src="event.logo" alt="event logo" class="ticket-logo">
<div class="ticket-name">
<h3>{{ event.name }}</h3>
<p v-html="event.shortDescription"></p>
</div>
<div class="ticket-info">
<p>Free seats: {{ event.free }} / {{ event.totalSeats }}</p>
<p>{{ event.locationName }}, {{ event.dateString }}</p>
<h4>Starting at {{ event.currency }} {{ event.startingPrice }}</h4>
</div>
</router-link>
</div>
</div>
<div class="order-app" v-else>
No future events are available!
</div>
</div>
</template>
<style scoped>
.order-app {
text-align: justify;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.ticket {
display: flex;
align-items: center;
flex-shrink: 0;
justify-content: center;
text-decoration: none;
color: var( --primary-color );
border-color: var( --primary-color );
border-width: 1px;
height: fit-content;
border-style: solid;
padding: 10px;
transition: 0.4s;
flex-direction: column;
}
.ticket-logo {
height: 20vh;
width: 20vh;
min-width: 20vh;
margin-right: 3%;
}
.ticket:hover {
background-color: var( --hover-color );
transition: 0.4s;
}
.ticket-name {
text-align: center;
}
.ticket-info {
text-align: center;
}
@media only screen and (min-width: 999px) {
.ticket {
flex-direction: row;
}
.ticket-name {
flex-shrink: 0;
margin-right: 3%;
width: 40%;
text-align: justify;
}
.ticket-info {
margin-right: auto;
text-align: justify;
}
}
</style>
<script>
export default {
name: 'OrderView',
methods: {
setActiveTicket ( id ) {
sessionStorage.setItem( 'selectedTicket', id );
sessionStorage.setItem( 'ticketData', JSON.stringify( { 'description': this.events[ id ][ 'description' ], 'name': this.events[ id ][ 'name' ], 'locationName': this.events[ id ][ 'locationName' ], 'date': this.events[ id ][ 'date' ] } ) );
sessionStorage.setItem( 'hasSeatplan', this.events[ id ][ 'hasSeatplan' ] );
},
loadEvents () {
fetch( '/getAPI/getAllEvents' ).then( res => {
res.json().then( dat => {
this.events = dat ?? {};
for ( let event in dat ) {
if ( this.events[ event ][ 'description' ].length > 200 ) {
this.events[ event ][ 'shortDescription' ] = this.events[ event ][ 'description' ].slice( 0, 200 ) + '...';
} else {
this.events[ event ][ 'shortDescription' ] = this.events[ event ][ 'description' ];
}
this.events[ event ][ 'logo' ] = new URL( location.protocol + '//' + location.hostname + ':' + location.port + '/eventAssets/' + this.events[ event ].eventID + 'Logo.jpg' );
}
} );
} );
}
},
created() {
this.loadEvents();
},
data () {
return {
events: { 'test': { 'name': 'TestEvent', 'description': 'This is a description for the TestEvent to test multiline support and proper positioning of the Fields', 'free': 2, 'maxTickets': 2, 'date': '2023-08-31T09:00:00Z', 'startingPrice': 15, 'location': 'TestLocation', 'eventID': 'test', 'currency': 'CHF', 'logo': new URL( '/src/assets/logo.png', import.meta.url ).href }, 'test2': { 'name': 'TestEvent2', 'description': 'This is a description for the TestEvent to test multiline support and proper positioning of the Fields', 'freeSeats': 2, 'maxSeats': 2, 'date': '2023-08-15T09:00:00Z', 'startingPrice': 15, 'location': 'TestLocation', 'eventID': 'test2', 'currency': 'CHF', 'logo': new URL( '/src/assets/logo.png', import.meta.url ).href } },
today: new Date().getTime(),
locations: {},
};
},
computed: {
orderedEvents () {
let sorted = Object.keys( this.events ).sort( ( a, b ) => {
return new Date( this.events[ a ].date ).getTime() - new Date( this.events[ b ].date ).getTime();
} );
let rt = {};
for ( let element in sorted ) {
if ( new Date( this.events[ sorted[ element ] ].date ) > this.today ) {
rt[ sorted[ element ] ] = this.events[ sorted[ element ] ];
rt[ sorted[ element ] ][ 'dateString' ] = new Date( rt[ sorted[ element ] ][ 'date' ] ).toLocaleString();
}
}
return rt;
}
},
};
</script>
<style>
nav {
display: initial;
}
</style>

View File

@@ -1,144 +0,0 @@
<!-- eslint-disable no-undef -->
<!--
* libreevent - GuestPurchaseView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="wrapper">
<div class="content">
<h1 style="font-size: 250%;">Thank you for your purchase!</h1>
<p>The system is currently processing your order and you will be able to download your tickets within a moment's notice.</p>
<p>You will receive an email with your tickets within the next few minutes</p>
<a href="/tickets/tickets.pdf" class="submit" style="display: none; text-decoration: none;" id="manual-download" target="_blank">Download manually</a>
<button onclick="if ( confirm( 'Do you really want to leave this page? You will not be able to download the tickets directly and instead you will have to download them from the email.' ) ) {
location.href = '/' }" class="submit">Back to the home page</button>
</div>
<notifications ref="notification" location="bottomright" size="bigger"></notifications>
</div>
</template>
<script>
import notifications from '@/components/notifications/notifications.vue';
export default {
name: 'PaymentSuccessView',
components: {
notifications
},
methods: {
},
data() {
return {};
},
created() {
if ( window.EventSource ) {
setTimeout( () => {
let startNotification = this.$refs.notification.createNotification( 'Connecting to status service...', 20, 'progress', 'normal' );
let source = new EventSource( localStorage.getItem( 'url' ) + '/payments/status', { withCredentials: true } );
let self = this;
source.onmessage = ( e ) => {
if ( e.data === 'ready' ) {
self.$refs.notification.cancelNotification( startNotification );
self.$refs.notification.createNotification( 'Your tickets are ready! Starting download...', 10, 'progress', 'normal' );
localStorage.removeItem( 'cart' );
fetch( '/getAPI/reloadData' ).catch( () => {} );
setTimeout( () => {
open( '/tickets/tickets.pdf' );
source.close();
}, 500 );
setTimeout( () => {
$( '#manual-download' ).slideDown( 500 );
}, 2000 );
} else if ( e.data === 'paymentOk' ) {
self.$refs.notification.createNotification( 'Your payment has been marked as completed!', 5, 'ok', 'normal' );
}
};
source.onopen = () => {
self.$refs.notification.createNotification( 'Connected to status service', 5, 'ok', 'normal' );
self.$refs.notification.cancelNotification( startNotification );
};
source.addEventListener( 'error', function( e ) {
if ( e.eventPhase == EventSource.CLOSED ) source.close();
if ( e.target.readyState == EventSource.CLOSED ) {
self.$refs.notification.cancelNotification( startNotification );
self.$refs.notification.createNotification( 'Disconnected from status service', 20, 'info', 'normal' );
}
}, false );
}, 300 );
} else {
setTimeout( () => {
this.$refs.notification.createNotification( 'Unsupported browser detected. Ticket generation might take longer!', 20, 'warning', 'normal' );
}, 300 );
// ping server every 5s to check if ticket ready
this.serverPing = setInterval( () => {
fetch( '/payments/status/ping' ).then( res => {
if ( res.status === 200 ) {
res.json().then( data => {
if ( data ) {
if ( data.status === 'ready' ) {
open( '/tickets/get' );
} else if ( data.status === 'paymentOk' ) {
this.$refs.notification.createNotification( 'Your payment has been marked as completed!', 5, 'ok', 'normal' );
}
}
} );
} else {
console.error( 'Request failed' );
this.$refs.notification.createNotification( 'We are sorry, but an error occurred. You will not be redirected automatically', 300, 'error', 'normal' );
}
} ).catch( error => {
console.error( error );
this.$refs.notification.createNotification( 'We are sorry, but an error occurred. You will not be redirected automatically', 300, 'error', 'normal' );
} );
}, 5000 );
}
}
};
</script>
<style>
.wrapper {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
}
.content {
width: 70%;
}
.small {
font-size: 75%;
color: rgb(158, 158, 158);
}
.submit {
margin-top: 2%;
background: linear-gradient(90deg, rgb(30, 36, 131), rgb(87, 66, 184), rgb(105, 115, 214), rgb(30, 36, 131), rgb(41, 128, 109), rgb(146, 50, 47));
background-size: 300px;
padding: 10px 20px;
border: none;
border-radius: 20px;
cursor: pointer;
transition: all 3s;
font-size: 75%;
color: white;
}
.submit:hover {
background-size: 200%;
background-position: -100%;
}
</style>

View File

@@ -1,328 +0,0 @@
<!--
* libreevent - PurchaseView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="purchase">
<h1>Purchase</h1>
<div class="purchase-app" v-if="cartNotEmpty">
<div v-if="!isAuthenticated" class="wrapper-buttons">
<router-link to="/login" class="option-button" @click="setRedirect()">Log in with an existing account</router-link><br>
<router-link to="/signup" class="option-button" @click="setRedirect()">Create new account</router-link><br>
<!-- <router-link to="/guest" v-if="!settings.accountRequired" class="option-button" @click="setRedirect()">Purchase as guest</router-link> -->
</div>
<div v-else class="wrapper">
<p>Ready to buy? Please once again check that all the right items are in your cart.</p>
<div class="cart-list">
<h2>Order summary</h2>
<h3>Your tickets</h3>
<div v-for="event in cart">
<h3>{{ event.displayName }}</h3>
<table class="tickets-table">
<tr v-for="ticket in event.tickets">
<td>
<h4 class="price"><div style="display: inline;" v-if="ticket.count">{{ ticket.count }}x</div> {{ ticket.displayName }}: </h4>
</td>
<td>
{{ backend.currency }} {{ ticket.price }}
</td>
</tr>
</table>
</div>
<div class="tool-wrapper wrapper-loggedIn">
<h4>Total: {{ backend.currency }} {{ backend.total }}</h4>
</div>
</div>
<button id="buy-button" @click="preparePayment();">Buy now</button>
</div>
</div>
<div v-else>
Cart is empty. Please add tickets <router-link to="/tickets">here</router-link>
<div class="empty-cart-wrapper">
<span class="material-symbols-outlined empty-cart">remove_shopping_cart</span>
</div>
</div>
<notifications ref="notification" location="topleft" size="bigger"></notifications>
<popups ref="popups" size="small" @data="data => { verifyTicketDelete( data.status ) }"></popups>
</div>
</template>
<style scoped>
#buy-button {
background-color: var( --accent-background );
padding: 2% 4%;
border-radius: 50px;
color: var( --secondary-color );
text-decoration: none;
transition: all 0.5s;
font-size: 100%;
margin-top: 4%;
cursor: pointer;
}
#buy-button:hover {
margin-top: 2%;
border-radius: 20px;
padding: 3% 6%;
font-size: 130%;
background-color: var( --accent-background-hover );
}
.purchase {
height: 100%;
display: flex;
flex-grow: 1;
flex-direction: column;
}
.purchase-app {
text-align: justify;
width: 100%;
}
.option-button {
border-style: solid;
border-color: var( --primary-color );
border-radius: 20px;
padding: 6% 7%;
display: block;
width: 60%;
text-align: center;
margin: 0.5%;
color: var( --primary-color );
text-decoration: none;
}
.option-button:hover {
background-color: var( --hover-color );
color: var( --secondary-color )
}
.wrapper-buttons {
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
height: 100%;
}
.wrapper {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.cart-list {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.tool-wrapper {
display: flex;
width: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
}
.price {
margin: 0;
padding: 0;
}
.empty-cart-wrapper {
width: 100%;
height: 70vh;
display: flex;
justify-content: center;
align-items: center;
}
.empty-cart {
display: block;
font-size: 20rem;
}
</style>
<script>
import { useUserStore } from '@/stores/userStore';
import { useBackendStore } from '@/stores/backendStore';
import { mapStores } from 'pinia';
import notifications from '@/components/notifications/notifications.vue';
import popups from '@/components/notifications/popups.vue';
export default {
name: 'PurchaseView',
data () {
return {
settings: { 'accountRequired': true, 'requiresAddress': true, 'requiresAge': true, 'requiresSpecialToken': true, 'specialRequirement': { 'display': { 'de': '', 'en': 'id number' }, 'rules': {} } },
isAuthenticated: false,
cart: {},
backend: { 'currency': 'CHF' },
cartNotEmpty: false,
userData: {},
};
},
components: {
notifications,
popups,
},
computed: {
...mapStores( useUserStore ),
...mapStores( useBackendStore )
},
methods: {
loadData () {
this.cartNotEmpty = false;
let cart = JSON.parse( localStorage.getItem( 'cart' ) );
for ( let event in cart ) {
if ( Object.keys( cart[ event ][ 'tickets' ] ).length ) {
this.cartNotEmpty = true;
}
}
if ( this.cartNotEmpty ) {
this.cart = cart;
this.isAuthenticated = this.userStore.getUserAuthenticated;
this.settings.accountRequired = !this.backendStore.getIsGuestPurchaseAllowed;
this.calculateTotal();
} else {
this.$router.push( '/tickets' );
}
for ( let event in this.cart ) {
this.seatChecks( event );
}
},
seatChecks ( event ) {
let self = this;
let allSeatsAvailable = true;
fetch( localStorage.getItem( 'url' ) + '/getAPI/getReservedSeats?event=' + event ).then( res => {
if ( res.status === 200 ) {
let unavailableSeats = {};
res.json().then( data => {
for ( let seat in data.reserved ) {
if ( data.reserved[ seat ] ) {
if ( !unavailableSeats[ data.reserved[ seat ].component ] ) {
unavailableSeats[ data.reserved[ seat ].component ] = {};
}
unavailableSeats[ data.reserved[ seat ].component ][ data.reserved[ seat ].id ] = 'nav';
}
}
for ( let seat in data.user ) {
if ( data.user[ seat ] ) {
if ( !unavailableSeats[ data.user[ seat ].component ] ) {
unavailableSeats[ data.user[ seat ].component ] = {};
}
unavailableSeats[ data.user[ seat ].component ][ data.user[ seat ].id ] = 'sel';
}
}
let tickets = {};
if ( this.cart[ event ] ) {
tickets = this.cart[ event ][ 'tickets' ];
}
if ( data.user ) {
for ( let element in tickets ) {
if ( !data.user[ element ] ) {
allSeatsAvailable = false;
if ( Object.keys( this.cart[ event ][ 'tickets' ] ).length > 1 ) {
delete this.cart[ event ][ 'tickets' ][ element ];
} else {
delete this.cart[ event ];
}
}
}
} else {
delete this.cart[ event ];
allSeatsAvailable = false;
}
this.unavailableSeats = unavailableSeats;
if ( !allSeatsAvailable ) {
setTimeout( () => {
self.$refs.popups.openPopup( 'We are sorry to tell you that an error occurred in the system and all sessions have been reset. You will need to pick the seats again. We are very sorry for the inconvenience', {}, 'string' );
}, 500 );
localStorage.setItem( 'cart', JSON.stringify( this.cart ) );
}
} );
} else {
console.error( 'unable to load' );
}
} );
},
calculateTotal () {
this.backend[ 'total' ] = 0;
for ( let event in this.cart ) {
for ( let ticket in this.cart[ event ][ 'tickets' ] ) {
this.backend[ 'total' ] += parseInt( this.cart[ event ][ 'tickets' ][ ticket ][ 'price' ] ) * parseInt( this.cart[ event ][ 'tickets' ][ ticket ][ 'count' ] ?? 1 );
}
}
},
setRedirect () {
sessionStorage.setItem( 'redirect', '/purchase' );
},
preparePayment () {
/*
How it works: Request to payment handling route of server -> get URL for payment
-> Redirect to that URL -> On completion payment provider redirects to complete
route (plain HTML document) which then awaits processing completion and gives the
user a link to download the ticket. A mail has been sent to user automatically.
*/
let prep = this.$refs.notification.createNotification( 'Preparing payment...', 20, 'progress', 'normal' );
let fetchOptions = {
method: 'post',
body: JSON.stringify( this.userData ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( '/payments/prepare', fetchOptions ).then( res => {
if ( res.status === 200 ) {
this.$refs.notification.cancelNotification( prep );
this.$refs.notification.createNotification( 'Payment prepared, redirecting...', 5, 'progress', 'high' );
res.text().then( text => {
setTimeout( () => {
window.location.href = text;
}, 300 );
} );
} else if ( res.status === 428 ) {
res.text().then( text => {
if ( text === 'ERR_MAIL_UNCONFIRMED' ) {
this.$refs.notification.cancelNotification( prep );
this.$refs.notification.createNotification( 'Please confirm your email address to proceed', 10, 'error', 'high' );
}
} );
} else {
this.$refs.notification.cancelNotification( prep );
this.$refs.notification.createNotification( 'An error occurred during preparation of payments. Please try again.', 10, 'error', 'high' );
}
} ).catch( err => {
console.error( err );
this.$refs.notification.cancelNotification( prep );
this.$refs.notification.createNotification( 'An error occurred during preparation of payments. Please try again.', 10, 'error', 'high' );
} );
}
},
created () {
this.loadData();
fetch( localStorage.getItem( 'url' ) + '/getAPI/extendTicketDeletion' );
}
};
</script>

View File

@@ -1,183 +0,0 @@
<!--
* libreevent - TicketsDetailsView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="details">
<div class="top-container" :style="`background-image: url( ${ event.banner } ); background-repeat: no-repeat; background-size: cover; background-position: center;`">
<img :src="event.logo" alt="Event Logo" class="event-logo">
<h1 class="eventTitle">{{ event.name }}</h1>
<router-link to="/tickets" class="back-button"><span class="material-symbols-outlined" style="font-size: 100%;">arrow_back</span></router-link>
</div>
<div class="container">
<div class="main">
<p v-for="line in event.description.split( '\n' )">{{ line }}</p>
</div>
<div class="aside">
<h3>{{ event.location }}</h3>
<p>{{ new Date( event.date ).toLocaleString() }}</p>
<router-link to="/tickets/order" class="ticket-button">Order tickets</router-link>
</div>
</div>
</div>
</template>
<style scoped>
.details {
height: 100%;
}
.top-container {
height: 45vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-size: 150%;
margin-bottom: 2.5%;
}
.eventTitle {
background-color: rgba(0, 0, 0, 0.6);
color: white;
padding: 1.5% 3%;
margin: 0;
}
.event-logo {
display: block;
height: 20vh;
width: 20vh;
margin-bottom: 5%;
}
.back-button {
color: white;
background-color: rgb(31, 31, 31);
padding: 10px;
border-radius: 100%;
display: flex;
justify-content: center;
align-items: center;
width: fit-content;
height: fit-content;
text-decoration: none;
position: absolute;
left: 2vh;
top: calc( 87px + 1vh );
}
.container {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap-reverse;
width: 100%;
flex-direction: row;
}
.main {
width: 100%;
margin-top: 5%;
}
.aside {
width: 80%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
background-color: gray;
padding-bottom: 2%;
margin-top: 2.5%;
}
.ticket-button {
display: block;
text-decoration: none;
padding: 5%;
width: fit-content;
background-color: var( --accent-color );
color: var( --secondary-color );
transition: all 1s;
border-radius: 50px;
}
.ticket-button:hover {
background-color: var( --accent-background-hover );
border-radius: 10px;
}
@media only screen and (min-width: 999px) {
.aside {
width: 25%;
display: flex;
justify-content: flex-start !important;
margin: 0;
margin-right: auto;
}
.main {
margin: 0;
width: 50%;
height: 100%;
margin-right: auto;
margin-left: auto;
text-align: left;
}
.container {
align-items: flex-end;
}
.event-logo {
position: absolute;
left: 20px;
top: calc( 87px + 2.5vh );
height: 40vh;
width: 40vh;
}
.top-container {
height: 45vh;
flex-direction: row;
font-size: 200%;
}
.back-button {
position: fixed;
left: 2vh;
top: 2vh;
}
}
</style>
<script>
export default {
name: 'TicketsDetailsView',
created () {
if ( !sessionStorage.getItem( 'selectedTicket' ) ) {
this.$router.push( '/tickets' );
}
this.eventID = sessionStorage.getItem( 'selectedTicket' );
this.event[ 'banner' ] = localStorage.getItem( 'url' ) + '/eventAssets/' + this.eventID + 'Banner.jpg';
this.event[ 'logo' ] = localStorage.getItem( 'url' ) + '/eventAssets/' + this.eventID + 'Logo.jpg';
const eventData = JSON.parse( sessionStorage.getItem( 'ticketData' ) );
this.event.name = eventData[ 'name' ];
this.event.date = eventData[ 'date' ];
this.event.description = eventData[ 'description' ];
this.event.location = eventData[ 'locationName' ];
},
data() {
return {
event: {},
};
}
};
</script>

View File

@@ -1,78 +0,0 @@
<!--
* libreevent - TicketsOrderingView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="details">
<!-- Load correct component depending on what the event's config is -->
<seatplan :ticketID="eventID" v-if="hasSeatplan"></seatplan>
<noseatplan :ticketID="eventID" v-else></noseatplan>
<router-link to="/tickets/details" class="back-button"><span class="material-symbols-outlined" style="font-size: 200%;">arrow_back</span></router-link>
</div>
</template>
<style scoped>
.details {
flex-grow: 1;
}
.back-button {
color: white;
background-color: rgb(31, 31, 31);
padding: 10px;
border-radius: 100%;
display: flex;
justify-content: center;
align-items: center;
width: fit-content;
height: fit-content;
text-decoration: none;
position: fixed;
left: 2vh;
bottom: 2vh;
}
@media only screen and (min-width: 999px) {
.back-button {
position: fixed;
left: 2vh;
top: 2vh;
}
}
</style>
<script>
import seatplan from '@/components/seatplan/userApp/userWindow.vue';
import noseatplan from '@/components/noseatplan.vue';
export default {
name: 'TicketsDetailsView',
components: {
seatplan,
noseatplan
},
data() {
return {
hasSeatplan: false,
eventID: '',
};
},
created () {
if ( !sessionStorage.getItem( 'selectedTicket' ) ) {
this.$router.push( '/tickets' );
}
this.eventID = sessionStorage.getItem( 'selectedTicket' );
if ( sessionStorage.getItem( 'hasSeatplan' ) === 'false' ) {
this.hasSeatplan = false;
} else {
this.hasSeatplan = true;
}
}
};
</script>

View File

@@ -1,269 +0,0 @@
<template>
<div>
<h1>Account</h1>
<p>Welcome, {{ accountData.first_name }} {{ accountData.name }}! <button @click="logout()">Log out</button></p>
<button @click="resendMailConfirmation()" v-if="!accountData.mail_confirmed">Resend confirmation email</button>
<div class="userData-wrapper">
<table class="userData">
<tr>
<td>
Email
</td>
<td>
<div v-if="!isEditingAccount">
{{ accountData.email }}
</div>
<div v-else>
<input type="email" name="email" id="email" v-model="accountData.email" @keyup="emailLiveChecker()"><br><br>
<p v-if="emailStatus" class="email-status">{{ emailStatus }}</p>
</div>
</td>
</tr>
<tr>
<td>
Name
</td>
<td>
<div v-if="!isEditingAccount">
{{ accountData.first_name }} {{ accountData.name }}
</div>
<div v-else>
<input type="text" name="first_name" id="first_name" v-model="accountData.first_name">
<input type="text" name="name" id="name" v-model="accountData.name">
</div>
</td>
</tr>
<tr>
<td>
Email notifications
</td>
<td>
<div v-if="!isEditingAccount">
<div v-if="accountData.marketing == 'true'">Enabled</div>
<div v-else>Disabled</div>
</div>
<select name="emailNotification" id="emailNotification" v-model="accountData.marketing" v-else>
<option value="true">Enabled</option>
<option value="false">Disabled</option>
</select>
</td>
</tr>
<tr v-if="accountData.mail_confirmed && twoFASetting !== 'disable'">
<td>
Two-Factor Authentication
</td>
<td>
<div v-if="!isEditingAccount">
<div v-if="accountData.two_fa == 'enhanced'">Enhanced</div>
<div v-else-if="accountData.two_fa == 'simple'">Simple</div>
<div v-else>Disabled</div>
</div>
<select name="two_fa" id="two_fa" v-model="accountData.two_fa" v-else>
<option value="enhanced">Enhanced</option>
<option value="simple">Simple</option>
<option value="disabled" v-if="twoFASetting === 'allow'">Disabled</option>
</select>
</td>
</tr>
</table>
<div>
<button @click="toggleEditing">
<div v-if="!isEditingAccount">Edit</div>
<div v-else>Cancel</div>
</button>
<button @click="save()" v-if="isEditingAccount">Save</button>
</div>
</div>
<notifications ref="notification" location="topright" size="bigger"></notifications>
<popups ref="popups" size="big" @data="data => { savePwd( data ) }"></popups>
</div>
</template>
<style>
nav {
display: initial;
}
</style>
<style scoped>
.userData-wrapper {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.userData {
width: 50%;
}
.email-status {
margin-top: -10px;
color: red;
font-style: italic;
margin-bottom: 20px;
}
</style>
<script>
import { useUserStore } from '@/stores/userStore';
import { mapStores } from 'pinia';
import notifications from '@/components/notifications/notifications.vue';
import popups from '@/components/notifications/popups.vue';
export default {
data () {
return {
accountData: {},
isEditingAccount: false,
emailStatus: '',
twoFASetting: 'allow',
};
},
components: {
notifications,
popups,
},
computed: {
...mapStores( useUserStore )
},
methods: {
logout() {
fetch( '/user/logout' ).then( () => {
this.$router.push( '/login' );
this.userStore.setUserAuth( false );
} );
},
toggleEditing () {
if ( this.isEditingAccount ) {
this.loadData();
} else {
this.emailLiveChecker();
}
this.isEditingAccount = !this.isEditingAccount;
},
save() {
let fetchOptions = {
method: 'post',
body: JSON.stringify( this.accountData ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/user/settings', fetchOptions ).then( res => {
if ( res.status === 200 ) {
this.$refs.notification.createNotification( 'Settings updated!', 5, 'ok', 'normal' );
this.isEditingAccount = false;
} else {
this.$refs.notification.createNotification( 'An error occurred whilst updating the settings. Please retry', 20, 'error', 'normal' );
}
} );
},
resendMailConfirmation() {
fetch( localStorage.getItem( 'url' ) + '/user/resendEmail' ).then( res => {
if ( res.status === 200 ) {
this.$refs.notification.createNotification( 'Confirmation email sent.', 5, 'ok', 'normal' );
} else {
this.$refs.notification.createNotification( 'An error occurred whilst sending the confirmation mail. Please retry', 20, 'error', 'normal' );
}
} );
},
emailLiveChecker () {
setTimeout( () => {
if ( this.checkEmail() ) {
this.emailStatus = '';
} else {
this.emailStatus = 'Invalid email address';
}
}, 100 );
},
checkEmail () {
const mail = this.accountData.email ?? '';
let stat = { 'atPos': 0, 'topLevelPos': 0 };
for ( let l in mail ) {
if ( stat[ 'atPos' ] > 0 ) {
if ( mail[ l ] === '@' ) {
return false;
} else if ( mail[ l ] === '.' ) {
if ( stat[ 'topLevelPos' ] > 0 ) {
if ( l > stat[ 'topLevelPos' ] + 2 ) {
stat[ 'topLevelPos' ] = parseInt( l );
} else {
return false;
}
} else {
if ( l > stat[ 'atPos' ] + 2 ) {
stat[ 'topLevelPos' ] = parseInt( l );
} else {
return false;
}
}
} else if ( !( /[a-z]/.test( mail[ l ] ) || /[A-Z]/.test( mail[ l ] ) || /[1-9]/.test( mail[ l ] ) || mail[ l ] === '-' || mail[ l ] === '_' ) ) {
return false;
}
} else {
if ( mail[ l ] === '@' ) {
if ( l > 2 ) {
stat[ 'atPos' ] = parseInt( l );
} else {
return false;
}
} else if ( !( /[a-z]/.test( mail[ l ] ) || /[A-Z]/.test( mail[ l ] ) || /[1-9]/.test( mail[ l ] ) || mail[ l ] === '.' || mail[ l ] === '-' || mail[ l ] == '_' ) ) {
return false;
}
}
}
if ( mail.length > stat[ 'topLevelPos' ] + 2 && stat[ 'topLevelPos' ] > 0 && stat[ 'atPos' ] > 0 ) {
return true;
} else {
return false;
}
},
loadData () {
// TODO: FUTURE Also get all orders of user (using join functions)
fetch( localStorage.getItem( 'url' ) + '/settings/2fa' ).then( res => {
if ( res.status === 200 ) {
res.text().then( text => {
this.twoFASetting = text;
} );
}
} );
fetch( localStorage.getItem( 'url' ) + '/user/details' ).then( res => {
if ( res.status === 200 ) {
res.json().then( data => {
if ( data.status ) {
this.accountData = data.data;
if ( !this.accountData.two_fa ) {
this.accountData.two_fa = 'disabled';
}
if ( !data.data.mail_confirmed ) {
setTimeout( () => {
this.$refs.notification.createNotification( 'Your account is unverified. Please confirm your email using the link we have sent to your email!', 20, 'info', 'normal' );
}, 1000 );
}
} else {
this.userStore.setUserAuth( false );
this.userStore.setUser2fa( false );
this.$router.push( '/login' );
}
} );
} else if ( res.status === 403 || res.status === 404 || res.status === 500 ) {
this.userStore.setUserAuth( false );
this.userStore.setUser2fa( false );
this.$router.push( '/login' );
}
} ).catch( err => {
console.warn( '[ AccountView ] Loading failed with the following message: ' + err );
} );
if ( this.userStore.getUserTwoFACompliant ) {
this.userStore.setUser2fa( false );
}
}
},
created () {
this.loadData();
}
};
</script>

View File

@@ -1,137 +0,0 @@
<!--
* libreevent - LoginView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="login">
<div class="login-app">
<h1>Log in</h1>
<form>
<label for="mail">Email</label><br>
<input type="email" v-model="formData[ 'mail' ]" name="mail" id="mail" required class="input"><br><br>
<label for="password">Password</label><br>
<input type="password" v-model="formData[ 'password' ]" name="password" id="password" required class="input">
</form>
<button @click="login();" class="button">Log in</button>
<router-link to="/signup" class="button">Sign up instead</router-link>
<router-link to="/password-reset" class="button">Reset password</router-link>
</div>
<notifications ref="notification" location="topright" size="bigger"></notifications>
</div>
</template>
<script>
import { useUserStore } from '@/stores/userStore';
import { mapStores } from 'pinia';
import notifications from '@/components/notifications/notifications.vue';
export default {
data () {
return {
formData: {}
};
},
components: {
notifications,
},
computed: {
...mapStores( useUserStore )
},
methods: {
login () {
if ( this.formData.mail ) {
if ( this.formData.password ) {
let progress = this.$refs.notification.createNotification( 'Logging you in', 20, 'progress', 'normal' );
let fetchOptions = {
method: 'post',
body: JSON.stringify( this.formData ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/user/login', fetchOptions ).then( res => {
res.json().then( json => {
if ( json.status === 'ok' ) {
this.userStore.setUserAuth( true );
this.$router.push( sessionStorage.getItem( 'redirect' ) ?? '/account' );
sessionStorage.removeItem( 'redirect' );
} else if ( json.status === '2fa' ) {
this.userStore.setUser2fa( true );
this.$router.push( '/twoFactors' );
} else if ( json.status === '2fa+' ) {
this.userStore.setUser2fa( true );
sessionStorage.setItem( '2faCode', json.code );
this.$router.push( '/twoFactors' );
} else {
this.$refs.notification.cancelNotification( progress );
this.$refs.notification.createNotification( 'The credentials you provided do not match our records.', 5, 'error', 'normal' );
}
} );
} );
} else {
this.$refs.notification.createNotification( 'A password is required to log in', 5, 'error', 'normal' );
}
} else {
this.$refs.notification.createNotification( 'An email address is required to log in', 5, 'error', 'normal' );
}
},
},
};
</script>
<style scoped>
.login {
background-image: url( '/otherAssets/background-login.webp' );
background-size: cover;
background-position: center;
width: 100%;
height: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 0 1 auto;
}
.login-app {
background-color: var( --background-color );
min-height: fit-content;
min-height: fit-content;
padding: 5% 20%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 50px;
}
.input {
width: 100%;
padding: 10px;
border-radius: 500px;
border: solid 1px black;
margin-top: 1%;
}
.button {
padding: 5px 10px;
margin-top: 2%;
}
nav {
display: initial;
}
#missing-email, #missing-password, #credentials-wrong {
display: none;
margin-bottom: 20px;
}
</style>

View File

@@ -1,68 +0,0 @@
<!--
* libreevent - PasswordResetView.vue
*
* Created by Janis Hutz 08/10/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h1>Password Reset</h1>
<p>Please enter the email address connected to your account to begin the password reset process.</p>
<input type="email" v-model="email" class="input"><br>
<button @click="reset()" class="button">Reset</button>
<notifications ref="notification" location="topright" size="bigger"></notifications>
</div>
</template>
<style scoped>
.input {
width: 50%;
padding: 10px;
border-radius: 500px;
border: solid 1px black;
margin-top: 1%;
}
</style>
<script>
import notifications from '@/components/notifications/notifications.vue';
export default {
data () {
return {
email: '',
};
},
components: {
notifications,
},
methods: {
reset() {
const startNotification = this.$refs.notification.createNotification( 'Starting password reset', 20, 'progress', 'normal' );
let fetchOptions = {
method: 'post',
body: JSON.stringify( {'email': this.email } ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/API/resetPW', fetchOptions ).then( res => {
if ( res.status !== 200 ) {
this.$refs.notification.cancelNotification( startNotification );
this.$refs.notification.createNotification( 'An account with this email address does not exist.', 5, 'error', 'normal' );
} else {
this.$refs.notification.cancelNotification( startNotification );
this.$refs.notification.createNotification( 'Password reset email sent. Please follow the instructions given there.', 30, 'ok', 'normal' );
setTimeout( () => {
location.href = '/login';
}, 10000 );
}
} );
}
}
};
</script>

View File

@@ -1,261 +0,0 @@
<!--
* libreevent - SignupView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="login">
<div class="login-app">
<h1>Sign up</h1>
<div class="parent">
<div class="field">
<label for="mail">Email</label><br>
<input type="email" v-model="formData[ 'mail' ]" name="mail" id="mail" required @keyup="emailLiveChecker()" class="input"><br><br>
<p v-if="emailStatus" class="email-status">{{ emailStatus }}</p>
</div>
<div class="field">
<label for="country">Country</label><br>
<input type="text" v-model="formData[ 'country' ]" name="country" id="country" required class="input"><br><br>
</div>
<div class="field">
<label for="firstName">First name</label><br>
<input type="text" v-model="formData[ 'firstName' ]" name="firstName" id="firstName" required class="input"><br><br>
</div>
<div class="field">
<label for="name">Last name</label><br>
<input type="text" v-model="formData[ 'name' ]" name="name" id="name" required class="input"><br><br>
</div>
<div class="field">
<label for="password">Password</label><br>
<input type="password" v-model="formData[ 'password' ]" name="password" id="password" required class="input"><br><br>
</div>
<div class="field">
<label for="password2">Confirm password</label><br>
<input type="password" v-model="formData[ 'password2' ]" name="password2" id="password2" required class="input"><br><br>
</div>
<div class="field">
<label for="news">Do you want to potentially get newsletter?</label><br>
</div>
<div class="field">
<input type="checkbox" v-model="formData[ 'newsletter' ]" name="news" id="news"><br><br>
</div>
</div>
<notifications ref="notification" location="topright" size="bigger"></notifications>
<button @click="signup();" class="button">Sign up</button>
<router-link to="/login" class="button">Log in instead</router-link>
</div>
</div>
</template>
<script>
import { useUserStore } from '@/stores/userStore';
import { mapStores } from 'pinia';
import notifications from '@/components/notifications/notifications.vue';
export default {
data () {
return {
formData: {},
emailStatus: '',
};
},
components: {
notifications,
},
computed: {
...mapStores( useUserStore )
},
methods: {
emailLiveChecker () {
setTimeout( () => {
if ( this.checkEmail() ) {
this.emailStatus = '';
} else {
this.emailStatus = 'Invalid email address';
}
}, 100 );
},
checkEmail () {
const mail = this.formData.mail ?? '';
let stat = { 'atPos': 0, 'topLevelPos': 0 };
for ( let l in mail ) {
if ( stat[ 'atPos' ] > 0 ) {
if ( mail[ l ] === '@' ) {
return false;
} else if ( mail[ l ] === '.' ) {
if ( stat[ 'topLevelPos' ] > 0 ) {
if ( l > stat[ 'topLevelPos' ] + 2 ) {
stat[ 'topLevelPos' ] = parseInt( l );
} else {
return false;
}
} else {
if ( l > stat[ 'atPos' ] + 2 ) {
stat[ 'topLevelPos' ] = parseInt( l );
} else {
return false;
}
}
} else if ( !( /[a-z]/.test( mail[ l ] ) || /[A-Z]/.test( mail[ l ] ) || /[1-9]/.test( mail[ l ] ) || mail[ l ] === '-' || mail[ l ] === '_' ) ) {
return false;
}
} else {
if ( mail[ l ] === '@' ) {
if ( l > 2 ) {
stat[ 'atPos' ] = parseInt( l );
} else {
return false;
}
} else if ( !( /[a-z]/.test( mail[ l ] ) || /[A-Z]/.test( mail[ l ] ) || /[1-9]/.test( mail[ l ] ) || mail[ l ] === '.' || mail[ l ] === '-' || mail[ l ] == '_' ) ) {
return false;
}
}
}
if ( mail.length > stat[ 'topLevelPos' ] + 2 && stat[ 'topLevelPos' ] > 0 && stat[ 'atPos' ] > 0 ) {
return true;
} else {
return false;
}
},
signup () {
if ( !this.formData.mail ) {
this.$refs.notification.createNotification( 'An email address is required to sign up', 5, 'error', 'normal' );
return;
}
if ( !this.formData.password ) {
this.$refs.notification.createNotification( 'A password is required to sign up', 5, 'error', 'normal' );
return;
}
if ( !this.formData.password2 ) {
this.$refs.notification.createNotification( 'Please confirm your password using the "Confirm password field"', 5, 'error', 'normal' );
return;
}
if ( this.formData.password !== this.formData.password2 ) {
this.$refs.notification.createNotification( 'The passwords provided do not match. Please ensure they are the same', 5, 'error', 'normal' );
return;
}
if ( !this.formData.country ) {
this.$refs.notification.createNotification( 'Please provide the country you live in.', 5, 'error', 'normal' );
return;
}
if ( !this.formData.name && !this.formData.firstName ) {
this.$refs.notification.createNotification( 'Please provide your first and last name!', 5, 'error', 'normal' );
return;
}
if ( !this.checkEmail() ) {
this.$refs.notification.createNotification( 'This email address is not an email address', 5, 'error', 'normal' );
return;
}
let progress = this.$refs.notification.createNotification( 'Signing up...', 20, 'progress', 'normal' );
let fetchOptions = {
method: 'post',
body: JSON.stringify( this.formData ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/user/signup', fetchOptions ).then( res => {
res.text().then( status => {
if ( status === 'ok' ) {
this.$refs.notification.cancelNotification( progress );
this.$refs.notification.createNotification( 'Signed up successfully. We have sent you an email. Please confirm it to finish sign-up', 5, 'ok', 'normal' );
setTimeout( () => {
this.userStore.setUserAuth( true );
this.$router.push( sessionStorage.getItem( 'redirect' ) ?? '/account' );
sessionStorage.removeItem( 'redirect' );
}, 5000 );
} else if ( status === 'exists' ) {
this.$refs.notification.cancelNotification( progress );
this.$refs.notification.createNotification( 'An account with this email address already exists. Please log in using it.', 5, 'error', 'normal' );
this.$refs.notification.createNotification( 'If you do not remember your password, reset it!', 5, 'error', 'normal' );
} else {
console.log( status );
}
} );
} );
}
},
};
</script>
<style scoped>
.parent {
display: flex;
justify-content: center;
flex-direction: row;
flex-wrap: wrap;
}
.field {
width: 90%;
margin-bottom: 2%;
}
.email-status {
margin-top: -10px;
color: red;
font-style: italic;
margin-bottom: 20px;
}
.login {
width: 100%;
display: flex;
}
.login-app {
background-color: var( --background-color );
margin-top: 0;
width: 100%;
height: 100%;
padding: 5% 5%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.button {
padding: 5px 10px;
margin-top: 2%;
}
.input {
width: 80%;
padding: 10px;
border-radius: 500px;
border: solid 1px black;
margin-top: 1%;
}
@media only screen and (min-width: 999px) {
.login {
background-image: url( '/otherAssets/background-login.webp' );
background-size: cover;
background-position: center;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.login-app {
width: 50vw;
height: min-content;
border-radius: 50px;
}
.field {
width: 40%;
}
}
</style>

View File

@@ -1,141 +0,0 @@
<template>
<div id="twoFA">
<h1>Two-Factor Authentication</h1>
<p>We have sent you an email containing a link for Authentication.</p>
<div class="code-container" v-if="code[ 1 ] != ''">
<p>Open the link in the email and enter this code:</p>
<div class="code">
<div class="code-sub" id="code-part1">{{ code[ 1 ] }}</div>
<div class="code-sub" id="code-part2">{{ code[ 2 ] }}</div>
</div>
</div>
<notifications ref="notification" location="bottomright" size="bigger"></notifications>
</div>
</template>
<script>
import notifications from '@/components/notifications/notifications.vue';
import { useUserStore } from '@/stores/userStore';
import { mapStores } from 'pinia';
export default {
name: 'twoFA',
components: {
notifications
},
data () {
return {
code: { '1': '', '2': '' },
serverPing: null,
};
},
computed: {
...mapStores( useUserStore ),
},
created () {
if ( this.userStore.getUserTwoFACompliant ) {
if ( window.EventSource ) {
setTimeout( () => {
let startNotification = this.$refs.notification.createNotification( 'Connecting to status service', 20, 'progress', 'normal' );
let source = new EventSource( localStorage.getItem( 'url' ) + '/user/2fa/check', { withCredentials: true } );
let self = this;
source.onmessage = ( e ) => {
if ( e.data === 'authenticated' ) {
self.userStore.setUserAuth( true );
self.$router.push( sessionStorage.getItem( 'redirect' ) ?? '/account' );
}
};
source.onopen = () => {
self.$refs.notification.createNotification( 'Connected to status service', 5, 'ok', 'normal' );
self.$refs.notification.cancelNotification( startNotification );
};
source.addEventListener( 'error', function( e ) {
if ( e.eventPhase == EventSource.CLOSED ) source.close();
if ( e.target.readyState == EventSource.CLOSED ) {
self.$refs.notification.cancelNotification( startNotification );
self.$refs.notification.createNotification( 'Could not connect to status service', 5, 'error', 'normal' );
}
}, false );
}, 300 );
} else {
setTimeout( () => {
this.$refs.notification.createNotification( 'Unsupported browser detected. Redirection might take longer to occur!', 20, 'warning', 'normal' );
}, 300 );
// ping server every 5s to check if logged in
this.serverPing = setInterval( () => {
fetch( '/user/2fa/ping' ).then( res => {
if ( res.status === 200 ) {
res.json().then( data => {
if ( data ) {
if ( data.status === 'ok' ) {
this.userStore.setUserAuth( true );
this.$router.push( sessionStorage.getItem( 'redirect' ) ?? '/account' );
}
}
} );
} else {
console.error( 'Request failed' );
this.$refs.notification.createNotification( 'We are sorry, but an error occurred. You will not be redirected automatically', 300, 'error', 'normal' );
}
} ).catch( error => {
console.error( error );
this.$refs.notification.createNotification( 'We are sorry, but an error occurred. You will not be redirected automatically', 300, 'error', 'normal' );
} );
}, 5000 );
}
let code = sessionStorage.getItem( '2faCode' ) ? sessionStorage.getItem( '2faCode' ) : '';
this.code = { '1': code.slice( 0, 3 ), '2': code.substring( 3 ) };
} else {
if ( this.userStore.getUserAuthenticated ) {
this.$router.push( '/account' );
} else {
this.$router.push( '/login' );
}
}
},
unmounted() {
clearInterval( this.serverPing );
}
};
</script>
<style scoped>
#twoFA, .code-container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.code-container {
width: fit-content;
padding: 5% 8%;
border: var( --primary-color ) solid 2px;
border-radius: 10px;
margin-top: 3%;
background-color: var( --popup-color );
}
.code {
background-color: var( --hover-color );
padding: 7% 10%;
margin-bottom: 0;
width: fit-content;
border-radius: 10px;
font-size: 200%;
font-family: monospace;
display: block;
}
.code-sub {
display: inline-block;
}
#code-part2 {
margin-left: 7px;
}
</style>

View File

@@ -1,4 +0,0 @@
# Terms of Service
libreevent is a free and open source event management solution that is hosted by the operator of this website. The libreevent developers take no responsibility for the content of this website, nor are they affiliated with the seller in any shape or form. As Software licensed under the GNU General Public License Version 3, it comes with ABSOLUTELY NO WARRANTY to the extent permitted by applicable law.

View File

@@ -1,15 +0,0 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
const path = require( 'path' );
export default defineConfig( {
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve( __dirname, './src' ),
},
},
server: {
'port': 8081
}
} );

View File

@@ -1,4 +0,0 @@
const { defineConfig } = require( '@vue/cli-service' );
module.exports = defineConfig( {
transpileDependencies: true
} );

View File

@@ -1,73 +0,0 @@
/*
* libreevent - .eslintrc.js
*
* Created by Janis Hutz 02/26/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
module.exports = {
'env': {
'browser': true,
'commonjs': true,
'es2021': true,
'node': true
},
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended'
],
'overrides': [
],
'parserOptions': {
'ecmaVersion': 'latest'
},
'rules': {
'indent': [
'error',
4
],
'linebreak-style': [
'error',
'unix'
],
'quotes': [
'error',
'single'
],
'semi': [
'error',
'always'
],
'spaced-comment': [
'error',
'always'
],
'arrow-spacing': [
'error',
{ 'before': true, 'after': true }
],
'func-call-spacing': [
'error',
'never'
],
'keyword-spacing': [
'error',
{ 'before': true, 'after': true }
],
'key-spacing': [
'error',
{ 'mode': 'strict' }
],
'space-before-blocks': [
'error',
'always'
],
'space-in-parens': [
'error',
'always'
],
'no-var': 'error'
}
};

View File

@@ -1,28 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,3 +0,0 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

View File

@@ -1,35 +0,0 @@
# libreevent-setup
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

View File

@@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>No page title :: libreevent</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200">
<script defer src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +0,0 @@
{
"name": "libreevent-setup",
"version": "1.0.7",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
},
"dependencies": {
"pinia": "^2.1.3",
"vue": "^3.3.4",
"vue-router": "^4.2.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"eslint": "^8.39.0",
"eslint-plugin-vue": "^9.11.0",
"vite": "^4.5.3"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,180 +0,0 @@
<template>
<router-view v-slot="{ Component, route }">
<transition :name="route.meta.transition || 'scale'" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</template>
<style>
:root {
--primary-color: #2c3e50;
--accent-background: rgb(30, 30, 82);
--secondary-color: white;
--background-color: white;
--popup-color: rgb(224, 224, 224);
--accent-color: #42b983;
--hover-color: rgb(165, 165, 165);
--accent-background-hover: rgb(124, 140, 236);
--overlay-color: rgba(0, 0, 0, 0.7);
--inactive-color: rgb(100, 100, 100);
--highlight-backdrop: rgb(143, 134, 192);
--hint-color: rgb(174, 210, 221);
--PI: 3.14159265358979;
}
@media ( prefers-color-scheme: dark ) {
:root {
--primary-color: white;
--accent-background: rgb(56, 56, 112);
--secondary-color: white;
--background-color: rgb(32, 32, 32);
--popup-color: rgb(58, 58, 58);
--accent-color: #42b983;
--hover-color: rgb(83, 83, 83);
--accent-background-hover: #4380a8;
--overlay-color: rgba(104, 104, 104, 0.575);
--inactive-color: rgb(190, 190, 190);
--highlight-backdrop: rgb(85, 63, 207);
--hint-color: rgb(88, 91, 110);
}
}
::selection {
background-color: var( --highlight-backdrop );
color: var( --secondary-color );
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
#app {
transition: 0.5s;
background-color: var( --background-color );
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: var( --primary-color );
height: 100%;
display: flex;
flex-direction: column;
}
nav {
padding: 30px;
}
nav a {
font-weight: bold;
color: var( --primary-color );
}
nav a.router-link-exact-active {
color: #2080ca;
}
.scale-enter-active,
.scale-leave-active {
transition: all 0.5s ease;
}
.scale-enter-from,
.scale-leave-to {
opacity: 0;
transform: scale(0.9);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.4s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 48
}
.clr-open {
border: black solid 1px !important;
}
.button {
margin-top: 2%;
background: linear-gradient(90deg, rgb(30, 36, 131), rgb(87, 66, 184), rgb(105, 115, 214), rgb(30, 36, 131), rgb(41, 128, 109), rgb(146, 50, 47));
background-size: 300px;
padding: 10px 20px;
border: none;
border-radius: 20px;
cursor: pointer;
transition: all 3s;
font-size: 75%;
color: white;
margin-bottom: 5vh;
font-size: 125%;
}
.button:hover {
background-size: 200%;
background-position: -100%;
}
input {
width: 50%;
padding: 10px;
border-radius: 500px;
border-style: solid;
text-align: center;
margin-bottom: 20px;
}
select {
width: 50%;
text-align: center;
padding: 10px;
border-radius: 500px;
border-style: solid;
background-color: #b4d9ff;
}
.wrapper {
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.content {
width: 60vw;
font-size: 110%;
}
</style>
<script>
export default {
created () {
this.theme = localStorage.getItem( 'theme' ) ? localStorage.getItem( 'theme' ) : '';
if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches || this.theme === '&#9788;' ) {
document.documentElement.classList.add( 'dark' );
this.theme = '&#9788;';
} else {
document.documentElement.classList.add( 'light' );
this.theme = '&#9789;';
}
}
};
</script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 KiB

View File

@@ -1,281 +0,0 @@
<!-- eslint-disable no-undef -->
<template>
<div id="notifications" @click="handleNotifications();">
<div class="message-box" :class="[ location, size ]">
<div class="message-container" :class="messageType">
<span class="material-symbols-outlined types hide" v-if="messageType == 'hide'">question_mark</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'ok'" style="background-color: green;">done</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'error'" style="background-color: red;">close</span>
<span class="material-symbols-outlined types progress-spinner" v-else-if="messageType == 'progress'" style="background-color: blue;">progress_activity</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'info'" style="background-color: lightblue;">info</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'warning'" style="background-color: orangered;">warning</span>
<p class="message">{{ message }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'notifications',
props: {
location: {
type: String,
'default': 'topleft',
},
size: {
type: String,
'default': 'default',
}
// Size options: small, default (default option), big, bigger, huge
},
data () {
return {
notifications: {},
queue: [],
message: '',
messageType: 'hide',
notificationDisplayTime: 0,
notificationPriority: 'normal',
currentlyDisplayedNotificationID: 0,
currentID: { 'critical': 0, 'medium': 1000, 'low': 100000 },
displayTimeCurrentNotification: 0,
notificationScheduler: null,
};
},
methods: {
createNotification( message, showDuration, messageType, priority ) {
/*
Takes a notification options array that contains: message, showDuration (in seconds), messageType (ok, error, progress, info) and priority (low, normal, critical).
Returns a notification ID which can be used to cancel the notification. The component will throttle notifications and display
one at a time and prioritize messages with higher priority. Use vue refs to access these methods.
*/
let id = 0;
if ( priority === 'critical' ) {
this.currentID[ 'critical' ] += 1;
id = this.currentID[ 'critical' ];
} else if ( priority === 'normal' ) {
this.currentID[ 'medium' ] += 1;
id = this.currentID[ 'medium' ];
} else if ( priority === 'low' ) {
this.currentID[ 'low' ] += 1;
id = this.currentID[ 'low' ];
}
this.notifications[ id ] = { 'message': message, 'showDuration': showDuration, 'messageType': messageType, 'priority': priority, 'id': id };
this.queue.push( id );
console.log( 'scheduled notification: ' + id + ' (' + message + ')' );
if ( this.displayTimeCurrentNotification >= this.notificationDisplayTime ) {
this.handleNotifications();
}
return id;
},
cancelNotification ( id ) {
/*
This method deletes a notification and, in case the notification is being displayed, hides it.
*/
try {
delete this.notifications[ id ];
} catch ( error ) {
console.log( 'notification to be deleted is nonexistent or currently being displayed' );
}
try {
this.queue.splice( this.queue.indexOf( id ), 1 );
} catch {
console.debug( 'queue empty' );
}
if ( this.currentlyDisplayedNotificationID == id ) {
this.handleNotifications();
}
},
handleNotifications () {
/*
This methods should NOT be called in any other component than this one!
*/
this.displayTimeCurrentNotification = 0;
this.notificationDisplayTime = 0;
this.message = '';
this.queue.sort();
if ( this.queue.length > 0 ) {
this.message = this.notifications[ this.queue[ 0 ] ][ 'message' ];
this.messageType = this.notifications[ this.queue[ 0 ] ][ 'messageType' ];
this.priority = this.notifications[ this.queue[ 0 ] ][ 'priority' ];
this.currentlyDisplayedNotificationID = this.notifications[ this.queue[ 0 ] ][ 'id' ];
this.notificationDisplayTime = this.notifications[ this.queue[ 0 ] ][ 'showDuration' ];
delete this.notifications[ this.queue[ 0 ] ];
this.queue.reverse();
this.queue.pop();
$( '.message-box' ).css( 'z-index', 20 );
} else {
this.messageType = 'hide';
$( '.message-box' ).css( 'z-index', -1 );
}
}
},
created () {
this.notificationScheduler = setInterval( () => {
if ( this.displayTimeCurrentNotification >= this.notificationDisplayTime ) {
this.handleNotifications();
} else {
this.displayTimeCurrentNotification += 0.5;
}
}, 500 );
},
unmounted ( ) {
clearInterval( this.notificationScheduler );
}
};
</script>
<style scoped>
.message-box {
position: fixed;
z-index: -1;
color: white;
transition: all 0.5s;
width: 95vw;
right: 2.5vw;
top: 1vh;
height: 10vh;
}
.message-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
opacity: 1;
transition: all 0.5s;
cursor: default;
}
.types {
color: white;
border-radius: 100%;
margin-right: auto;
margin-left: 5%;
padding: 1.5%;
font-size: 200%;
}
.message {
margin-right: 5%;
text-align: end;
}
.ok {
background-color: rgb(1, 71, 1);
}
.error {
background-color: rgb(114, 1, 1);
}
.info {
background-color: rgb(44, 112, 151);
}
.warning {
background-color: orange;
}
.hide {
opacity: 0;
}
.progress {
z-index: 20;
background-color: rgb(0, 0, 99);
}
.progress-spinner {
animation: spin 2s infinite linear;
}
@keyframes spin {
from {
transform: rotate( 0deg );
}
to {
transform: rotate( 720deg );
}
}
@media only screen and (min-width: 750px) {
.default {
height: 10vh;
width: 32vw;
}
.small {
height: 7vh;
width: 27vw;
}
.big {
height: 12vh;
width: 38vw;
}
.bigger {
height: 15vh;
width: 43vw;
}
.huge {
height: 20vh;
width: 50vw;
}
.topleft {
top: 3vh;
left: 0.5vw;
}
.topright {
top: 3vh;
right: 0.5vw;
}
.bottomright {
bottom: 3vh;
right: 0.5vw;
}
.bottomleft {
top: 3vh;
right: 0.5vw;
}
}
@media only screen and (min-width: 1500px) {
.default {
height: 10vh;
width: 15vw;
}
.small {
height: 7vh;
width: 11vw;
}
.big {
height: 12vh;
width: 17vw;
}
.bigger {
height: 15vh;
width: 20vw;
}
.huge {
height: 20vh;
width: 25vw;
}
}
</style>

View File

@@ -1,12 +0,0 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
const app = createApp( App );
app.use( createPinia() );
app.use( router );
app.mount( '#app' );

View File

@@ -1,44 +0,0 @@
import { createRouter, createWebHistory } from 'vue-router';
import { useBackendStore } from '@/stores/backendStore';
import HomeView from '../views/HomeView.vue';
import setupRoutes from './setupRoutes';
let routes = [
{
path: '/',
name: 'home',
component: HomeView,
meta: {
title: 'Welcome to libreevent!'
}
},
{
path: '/admin/login',
name: 'adminLogin',
component: () => import( '../views/AdminLoginView.vue' ),
meta: {
title: 'Admin login - Restart required! :: libreevent'
}
},
];
routes.push( setupRoutes );
const router = createRouter( {
history: createWebHistory( import.meta.env.BASE_URL ),
routes: routes
} );
router.beforeEach( ( to ) => {
let backendStore = useBackendStore();
backendStore.loadVisitedSetupPages();
if ( to.name.substring( 0, 5 ) === 'setup' && !backendStore.getVisitedSetupPages[ to.name.substring( 5 ).toLowerCase() ] && to.name.substring( 5 ).toLowerCase() !== 'start' && to.name.substring( 5 ).toLowerCase() !== 'root' ) {
return { name: 'setupStart' };
}
} );
router.afterEach( ( to ) => {
document.title = to.meta.title ? to.meta.title : 'libreevent';
} );
export default router;

View File

@@ -1,56 +0,0 @@
/*
* libreevent - setupRoutes.js
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
export default {
path: '/setup',
name: 'setup',
component: () => import( '../views/SetupView.vue' ),
meta: {
title: 'Setup :: Admin - libreevent',
setupAuthRequired: true,
},
children: [
{
path: '',
name: 'setupStart',
component: () => import( '../views/SetupStartView.vue' ),
meta: {
title: 'Start :: Setup - libreevent',
setupAuthRequired: true,
}
},
{
path: 'basics',
name: 'setupBasics',
component: () => import( '../views/BasicSetupView.vue' ),
meta: {
title: 'Basic setup :: Setup - libreevent',
setupAuthRequired: true,
}
},
{
path: 'root',
name: 'setupRoot',
component: () => import( '../views/SetupRootView.vue' ),
meta: {
title: 'Root account :: Setup - libreevent',
setupAuthRequired: true,
}
},
{
path: 'complete',
name: 'setupComplete',
component: () => import( '../views/SetupCompleteView.vue' ),
meta: {
title: 'Setup complete :: Setup - libreevent',
setupAuthRequired: true,
}
},
]
};

View File

@@ -1,26 +0,0 @@
/*
* libreevent - backendStore.js
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
import { defineStore } from 'pinia';
export const useBackendStore = defineStore( 'backend', {
state: () => ( { 'visitedSetupPages': {} } ),
getters: {
getVisitedSetupPages: ( state ) => state.visitedSetupPages,
},
actions: {
addVisitedSetupPages ( page, data ) {
this.visitedSetupPages[ page ] = data;
sessionStorage.setItem( 'visitedSetupPages', JSON.stringify( this.visitedSetupPages ) );
},
loadVisitedSetupPages () {
this.visitedSetupPages = sessionStorage.getItem( 'visitedSetupPages' ) ? JSON.parse( sessionStorage.getItem( 'visitedSetupPages' ) ) : {};
}
}
} );

View File

@@ -1,38 +0,0 @@
<template>
<div class="wrapper">
<h1>Please restart libreevent!</h1>
<p>Restart the node.js app that libreevent runs in to access the login page. Not sure how to do this? Click <a href="https://libreevent.janishutz.com/docs/setup/setup#complete" target="_blank">here</a></p>
</div>
</template>
<style scoped>
.wrapper {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-size: 150%;
}
h1 {
font-size: 400%;
}
</style>
<script>
export default {
created() {
fetch( '/getSetupStatus' ).then( res => {
if ( res.status === 200 ) {
res.text().then( text => {
if ( text !== 'true' ) {
this.$router.push( '/' );
}
} );
}
} );
}
};
</script>

View File

@@ -1,164 +0,0 @@
<!--
* libreevent - BasicSetupView.vue
*
* Created by Janis Hutz 08/23/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="wrapper">
<div class="content">
<h1>Basic Setup</h1>
<p>To make setting up the database and email accounts easier, you can enter the required values below.</p>
<p>You may find more infos about this part <a href="https://libreevent.janishutz.com/docs/setup/setup#basic-setup" target="_blank">here</a></p>
<h2>General information</h2>
<label for="name">Name of the website</label><br>
<input type="text" name="name" id="name" v-model="formData.websiteName"><br>
<label for="name">URL of the website</label><br>
<input type="text" name="domain" id="domain" v-model="formData.yourDomain"><br>
<h2>Database</h2>
<p>A database is a piece of software that specializes in storing data. libreevent can use most SQL based databases as well as a custom JSON-based database. You are strongly encouraged to use a SQL based database, as they perform significantly better. Read more
<a href="https://libreevent.janishutz.com/docs/setup/installation#database" target="_blank">here</a>
</p>
<label for="dbType">Database type</label><br>
<select name="dbType" id="dbType" v-model="formData.dbType">
<option value="mysql">SQL-Database</option>
<option value="json">JSON-Database</option>
</select>
<form v-if="formData.dbType === 'mysql'">
<label for="host">Database host name (usually domain name or IP of webserver)</label><br>
<input type="url" name="host" id="host" v-model="formData.db.host"><br>
<label for="database">Database name</label><br>
<input type="text" name="database" id="database" v-model="formData.db.database"><br>
<label for="user">Database user</label><br>
<input type="text" name="user" id="user" v-model="formData.db.user"><br>
<label for="password">Password</label><br>
<input type="password" name="password" id="password" v-model="formData.db.password"><br>
<label for="port">Database port (default usually fine)</label><br>
<input type="number" name="port" id="port" min="1" max="65535" v-model="formData.db.port"><br>
</form>
<h2>Email</h2>
<p>An email address is required for libreevent to send out mails to users automatically, including their ticket and, in case Two-Factor-Authentication is enabled,
a Two-Factor-Authentication email.</p>
<h3>Account</h3>
<p>Here you have to enter the connection details for an email account. Most webhosting plans come with email addresses, so you might as well create a new one.
Note that you can customize how the sender of the mail appears down below in the display section.</p>
<form>
<label for="host">SMTP Server</label><br>
<input type="url" name="host" id="host" v-model="formData.email.host"><br>
<label for="port">SMTP Port (default usually fine)</label><br>
<input type="number" name="port" id="port" min="1" max="65535" v-model="formData.email.port"><br>
<label for="user">Email account name</label><br>
<input type="email" name="user" id="user" v-model="formData.email.user"><br>
<label for="pass">Password</label><br>
<input type="password" name="pass" id="pass" v-model="formData.email.pass"><br>
</form>
<h3>Display</h3>
<p>Here you can adjust how the email sender appears to the customer. This also means, that the email address shown below might receive a response if
a customer does not possess the ability to read, which might happen from time to time. All mails contain the information that one should not respond
to them.
</p>
<form>
<label for="display">Display name (what is shown to user in from field)</label><br>
<input type="url" name="display" id="display" v-model="formData.display"><br>
<label for="dpEmail">Email address to show</label><br>
<input type="text" name="dpEmail" id="dpEmail" v-model="formData.dpEmail"><br>
</form>
<button @click="submit()" class="button">Continue</button>
</div>
<notifications ref="notification" location="topright" size="bigger"></notifications>
</div>
</template>
<style scoped>
#dbType {
margin-bottom: 5%;
}
</style>
<script>
import { useBackendStore } from '@/stores/backendStore.js';
import { mapStores } from 'pinia';
import notifications from '../components/notifications.vue';
export default {
components: {
notifications,
},
data () {
return {
formData: {
'dbType': 'mysql',
'db': {
'port': 3306,
},
'email': {
'port': 587
},
'websiteName': 'libreevent',
},
};
},
computed: {
...mapStores( useBackendStore )
},
methods: {
submit() {
if ( this.formData.dbType === 'mysql' ) {
if ( !this.formData.db.port || !this.formData.db.host || !this.formData.db.database || !this.formData.db.user || !this.formData.db.password ) {
this.$refs.notification.createNotification( 'Database settings are not complete!', 5, 'error', 'normal' );
return;
}
}
if ( this.formData.email.port && this.formData.email.host && this.formData.email.user && this.formData.email.pass
&& this.formData.dpEmail && this.formData.display && this.formData.websiteName ) {
this.formData.mailDisplay = this.formData.display + ' <' + this.formData.dpEmail + '>';
let progressNot = this.$refs.notification.createNotification( 'Setting up...', 20, 'progress', 'normal' );
const options = {
method: 'post',
body: JSON.stringify( this.formData ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( '/setup/saveBasicSettings', options ).then( res => {
if ( res.status === 200 ) {
this.$refs.notification.cancelNotification( progressNot );
this.$refs.notification.createNotification( 'Saved!', 5, 'ok', 'normal' );
setTimeout( () => {
this.continue();
}, 2000 );
} else {
this.$refs.notification.createNotification( 'Setup key incorrect!', 5, 'error', 'normal' );
}
} );
} else {
this.$refs.notification.createNotification( 'Missing entries', 5, 'error', 'normal' );
return;
}
},
continue () {
sessionStorage.setItem( 'basics', JSON.stringify( this.formData ) );
this.backendStore.addVisitedSetupPages( 'root', true );
this.$router.push( '/setup/root' );
},
collectUrl() {
this.formData.yourDomain = location.protocol + '//' + location.host + ( location.port ? ':' + location.port : '' );
this.formData.db.host = location.hostname;
}
},
created () {
if ( sessionStorage.getItem( 'basics' ) ) {
this.formData = JSON.parse( sessionStorage.getItem( 'basics' ) );
}
this.collectUrl();
}
};
</script>

View File

@@ -1,86 +0,0 @@
<!--
* libreevent - HomeView.vue
*
* Created by Janis Hutz 07/17/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="home">
<img alt="Libreevent logo" src="../assets/logo.png">
<h1>Welcome to libreǝvent!</h1>
<p>Let's start the setup by entering the setup key below! You may define a setup key in the <i>setupkey.txt</i> file of libreevent. See <a href="https://libreevent.janishutz.com/docs/setup/installation" target="_blank">here</a> for more instructions</p>
<form>
<label for="key">Your setup key</label><br>
<input type="text" v-model="formData[ 'token' ]" required name="key" id="key">
</form>
<button @click="setup();" class="button">Start setup</button>
<notifications ref="notification" location="topright" size="bigger"></notifications>
</div>
</template>
<script>
import notifications from '../components/notifications.vue';
export default {
data() {
return {
formData: {},
};
},
components: {
notifications,
},
methods: {
setup () {
const options = {
method: 'post',
body: JSON.stringify( this.formData ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( '/setup/start', options ).then( res => {
if ( res.status === 200 ) {
this.$router.push( '/setup' );
} else {
this.$refs.notification.createNotification( 'Setup key incorrect!', 5, 'error', 'normal' );
}
} );
}
},
created() {
fetch( '/setup/getKeyStatus' ).then( res => {
if ( res.status === 200 ) {
res.text().then( text => {
if ( text === 'ok' ) {
this.$router.push( '/setup' );
}
} );
}
} );
}
};
</script>
<style scoped>
img {
height: 40vh;
}
.home {
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
form {
width: 50%;
}
</style>

Some files were not shown because too many files have changed in this diff Show More