mirror of
https://github.com/janishutz/libreevent.git
synced 2025-11-26 05:44:24 +00:00
restructuring
This commit is contained in:
4
src/webapp/main/.browserslistrc
Normal file
4
src/webapp/main/.browserslistrc
Normal file
@@ -0,0 +1,4 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
not ie 11
|
||||
23
src/webapp/main/.gitignore
vendored
Normal file
23
src/webapp/main/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
.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?
|
||||
19
src/webapp/main/README.md
Normal file
19
src/webapp/main/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# 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/).
|
||||
28
src/webapp/main/index.html
Normal file
28
src/webapp/main/index.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<!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>
|
||||
<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>
|
||||
19
src/webapp/main/jsconfig.json
Normal file
19
src/webapp/main/jsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
||||
14
src/webapp/main/notes.md
Normal file
14
src/webapp/main/notes.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Account view:
|
||||
- set page title based on settings
|
||||
|
||||
- make pricing groups changeable in UI (event categories)
|
||||
|
||||
- Fix text field overflow (text too big for box)
|
||||
- Other optimisation for seat plan editor
|
||||
|
||||
- Implement Permission system
|
||||
|
||||
|
||||
|
||||
|
||||
- add webpack to project website to decrease file size
|
||||
2307
src/webapp/main/package-lock.json
generated
Normal file
2307
src/webapp/main/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
src/webapp/main/package.json
Normal file
22
src/webapp/main/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "libreevent",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vite --host",
|
||||
"preview": "vite preview --host",
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pdfme/generator": "^1.2.3",
|
||||
"@pdfme/ui": "^1.2.3",
|
||||
"pinia": "^2.0.34",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.0.3",
|
||||
"vue3-draggable-resizable": "^1.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^1.10.2",
|
||||
"vite": "^2.5.4"
|
||||
}
|
||||
}
|
||||
1
src/webapp/main/public/coloris.min.css
vendored
Normal file
1
src/webapp/main/public/coloris.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
src/webapp/main/public/coloris.min.js
vendored
Normal file
6
src/webapp/main/public/coloris.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
src/webapp/main/public/favicon.ico
Normal file
BIN
src/webapp/main/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
24
src/webapp/main/public/iecrasher.html
Normal file
24
src/webapp/main/public/iecrasher.html
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
|
||||
<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>
|
||||
|
||||
0
src/webapp/main/public/lang/order/cart/en.json
Normal file
0
src/webapp/main/public/lang/order/cart/en.json
Normal file
0
src/webapp/main/public/lang/order/pay/en.json
Normal file
0
src/webapp/main/public/lang/order/pay/en.json
Normal file
0
src/webapp/main/public/lang/order/purchase/en.json
Normal file
0
src/webapp/main/public/lang/order/purchase/en.json
Normal file
0
src/webapp/main/public/lang/user/account/en.json
Normal file
0
src/webapp/main/public/lang/user/account/en.json
Normal file
0
src/webapp/main/public/lang/user/login/en.json
Normal file
0
src/webapp/main/public/lang/user/login/en.json
Normal file
0
src/webapp/main/public/lang/user/signup/en.json
Normal file
0
src/webapp/main/public/lang/user/signup/en.json
Normal file
191
src/webapp/main/src/App.vue
Normal file
191
src/webapp/main/src/App.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<!--
|
||||
* libreevent - App.vue
|
||||
*
|
||||
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
|
||||
* https://janishutz.com, development@janishutz.com
|
||||
*
|
||||
*
|
||||
-->
|
||||
|
||||
<template>
|
||||
<nav>
|
||||
<router-link to="/">Home</router-link> |
|
||||
<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>
|
||||
: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: 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;
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
--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);
|
||||
}
|
||||
|
||||
@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 );
|
||||
}
|
||||
|
||||
#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: #42b983;
|
||||
}
|
||||
|
||||
.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 === '☼' ) {
|
||||
document.documentElement.classList.remove( 'dark' );
|
||||
document.documentElement.classList.add( 'light' );
|
||||
localStorage.setItem( 'theme', '☽' );
|
||||
this.theme = '☽';
|
||||
} else if ( this.theme === '☽' ) {
|
||||
document.documentElement.classList.remove( 'light' );
|
||||
document.documentElement.classList.add( 'dark' );
|
||||
localStorage.setItem( 'theme', '☼' );
|
||||
this.theme = '☼';
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.theme = localStorage.getItem( 'theme' ) ? localStorage.getItem( 'theme' ) : '';
|
||||
if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches || this.theme === '☼' ) {
|
||||
document.documentElement.classList.add( 'dark' );
|
||||
this.theme = '☼';
|
||||
} else {
|
||||
document.documentElement.classList.add( 'light' );
|
||||
this.theme = '☽';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
BIN
src/webapp/main/src/assets/logo.png
Normal file
BIN
src/webapp/main/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
317
src/webapp/main/src/components/noseatplan.vue
Normal file
317
src/webapp/main/src/components/noseatplan.vue
Normal file
@@ -0,0 +1,317 @@
|
||||
<template>
|
||||
<div class="seatingWrapper">
|
||||
<div class="sidebar">
|
||||
<h2>{{ eventInfo.name }}</h2>
|
||||
<h3>{{ eventInfo.date }}</h3>
|
||||
<h3>{{ eventInfo.location }}</h3>
|
||||
<h3>Selected tickets</h3>
|
||||
<table class="price-table" v-for="event in selectedSeats">
|
||||
<tr v-if="Object.keys( event.selectedSeats ).length">
|
||||
<h4>{{ event.name }}</h4>
|
||||
</tr>
|
||||
<tr v-for="ticket in event.selectedSeats">
|
||||
<td>{{ ticket.name }} ({{ ticket.ageGroup }})</td>
|
||||
<td>{{ eventInfo[ 'currency' ] }} {{ ticket[ 'price' ] }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h3>Total: {{ eventInfo[ 'currency' ] }} {{ total }}</h3>
|
||||
<router-link to="/cart">To cart</router-link>
|
||||
</div>
|
||||
<div class="noseatplan">
|
||||
<h3>Available tickets</h3>
|
||||
<div class="wrapper">
|
||||
<div v-for="ticket in tickets">
|
||||
{{ eventInfo[ 'categories' ][ ticket.category ][ 'name' ] }}<br>
|
||||
<div v-for="ticketOption in eventInfo[ 'ageGroups' ]" class="ticket">
|
||||
<div>
|
||||
{{ ticketOption.name }} <div style="display: inline" v-if="ticketOption.age">({{ ticketOption.age }})</div> {{ eventInfo.currency }} {{ eventInfo[ 'categories' ][ ticket.category ][ 'price' ][ ticketOption.id ] }} <span class="material-symbols-outlined">add</span> Selected <span class="material-symbols-outlined">remove</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'noseatplan',
|
||||
props: {
|
||||
ticketID: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
tickets: { 'ticket1': { 'name': 'Ticket 1', 'id': 'ticket1', 'category': 1 }, 'ticket2': { 'name': 'Ticket 2', 'id': 'ticket2', 'category': 2 } },
|
||||
eventInfo: { 'name': 'TestEvent', 'location': 'TestLocation', 'date': 'TestDate', 'RoomName': 'TestRoom', '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 years' }, '2':{ 'id': 2, 'name': 'Adult', 'age': null } }, 'ageGroupCount':2, 'stage': true },
|
||||
selectedSeats: {},
|
||||
pricingCurrentlySelected: {},
|
||||
total: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadPreviouslySelected () {
|
||||
/*
|
||||
This function is called whenever the data on the webpage is to be reloaded
|
||||
*/
|
||||
|
||||
// load data from cart and set up cart if not available
|
||||
let cart = sessionStorage.getItem( 'cart' ) ? JSON.parse( sessionStorage.getItem( 'cart' ) ) : {};
|
||||
cart[ this.ticketID ?? 'default' ] = cart[ this.ticketID ?? 'default' ] ? cart[ this.ticketID ?? 'default' ] : { 'name': this.eventInfo.name, 'date': this.eventInfo.date, 'location': this.eventInfo.location, 'currency': this.eventInfo.currency };
|
||||
cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] = cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] ? cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] : {};
|
||||
|
||||
let data = cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] ? cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] : {};
|
||||
|
||||
let showError = false
|
||||
for ( let i in data ) {
|
||||
if ( this.seating[ data[ i ][ 'row' ] ][ 'content' ][ data[ i ][ 'seat' ] ][ 'available' ] ) {
|
||||
this.seating[ data[ i ][ 'row' ] ][ 'content' ][ data[ i ][ 'seat' ] ][ 'selected' ] = true;
|
||||
} else {
|
||||
showError = true;
|
||||
delete data[ i ];
|
||||
}
|
||||
}
|
||||
|
||||
if ( showError ) {
|
||||
// TODO: Show popup that no more tickets in a category are available
|
||||
}
|
||||
|
||||
|
||||
// check if no ticket selected and prevent writing if no ticket
|
||||
// selected to not show too many events
|
||||
let isEmpty = sessionStorage.getItem( 'selectedTicket' ) ? false : true;
|
||||
|
||||
if ( !isEmpty ) {
|
||||
cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] = data;
|
||||
sessionStorage.setItem( 'cart', JSON.stringify( cart ) );
|
||||
}
|
||||
|
||||
this.selectedSeats = cart;
|
||||
this.sumUp();
|
||||
},
|
||||
sumUp () {
|
||||
// This function calculates the total price of the tickets for this event.
|
||||
let cart = sessionStorage.getItem( 'cart' ) ? JSON.parse( sessionStorage.getItem( 'cart' ) ) : {};
|
||||
|
||||
let price = 0;
|
||||
for ( let i in cart ) {
|
||||
for ( let entry in cart[ i ][ 'selectedSeats' ] ) {
|
||||
price += parseInt( cart[ i ][ 'selectedSeats' ][ entry ][ 'price' ] );
|
||||
}
|
||||
}
|
||||
|
||||
let back = {};
|
||||
|
||||
back[ 'total' ] = price;
|
||||
back[ 'currency' ] = this.eventInfo.currency;
|
||||
|
||||
this.total = price;
|
||||
|
||||
|
||||
// check if no ticket selected and prevent writing if no ticket
|
||||
// selected to not show too many events
|
||||
let isEmpty = sessionStorage.getItem( 'selectedTicket' ) ? false : true;
|
||||
|
||||
if ( !isEmpty ) {
|
||||
sessionStorage.setItem( 'backend', JSON.stringify( back ) );
|
||||
sessionStorage.setItem( 'cart', JSON.stringify( cart ) );
|
||||
}
|
||||
},
|
||||
selectSeat( placeID, rowID ) {
|
||||
/*
|
||||
This function allows the user to select a seat and deselect it, if it has previously
|
||||
been selected.
|
||||
*/
|
||||
sessionStorage.setItem( 'tempStorage', JSON.stringify( { 1:[ placeID, rowID ] } ) );
|
||||
let cart = sessionStorage.getItem( 'cart' ) ? JSON.parse( sessionStorage.getItem( 'cart' ) ) : {};
|
||||
cart[ this.ticketID ?? 'default' ] = cart[ this.ticketID ?? 'default' ] ? cart[ this.ticketID ?? 'default' ] : {};
|
||||
cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] = cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] ? cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] : {};
|
||||
|
||||
let data = cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] ? cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] : {};
|
||||
|
||||
let isDeleting = false;
|
||||
|
||||
for ( let i in data ) {
|
||||
if ( data[ i ][ 'seat' ] == placeID && data[ i ][ 'row' ] == rowID ) {
|
||||
delete data[ i ];
|
||||
isDeleting = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.seating[ rowID ][ 'content' ][ placeID ][ 'selected' ] = !isDeleting;
|
||||
|
||||
if ( isDeleting ) {
|
||||
cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] = data;
|
||||
sessionStorage.setItem( 'cart', JSON.stringify( cart ) );
|
||||
this.selectedSeats = cart;
|
||||
this.sumUp();
|
||||
} else {
|
||||
if ( this.eventInfo.ageGroupCount > 1 ) {
|
||||
$( '#overlay' ).show( 200 );
|
||||
} else {
|
||||
this.storeSeat( '1' );
|
||||
}
|
||||
}
|
||||
|
||||
this.pricingCurrentlySelected = this.eventInfo[ 'categories' ][ this.seating[ rowID ][ 'content' ][ placeID ][ 'category' ] ][ 'price' ];
|
||||
},
|
||||
closePopup () {
|
||||
// This function closes the popup and sets the seat to not selected
|
||||
$( '#overlay' ).hide( 200 );
|
||||
let seat = JSON.parse( sessionStorage.getItem( 'tempStorage' ) );
|
||||
this.seating[ seat[ 1 ][ 1 ] ][ 'content' ][ seat[ 1 ][ 0 ] ][ 'selected' ] = false;
|
||||
},
|
||||
storeSeat( ticketOption ) {
|
||||
/*
|
||||
This function stores a ticket into the event's selected seat sessionStorage.
|
||||
*/
|
||||
|
||||
let cart = sessionStorage.getItem( 'cart' ) ? JSON.parse( sessionStorage.getItem( 'cart' ) ) : {};
|
||||
cart[ this.ticketID ?? 'default' ] = cart[ this.ticketID ?? 'default' ] ? cart[ this.ticketID ?? 'default' ] : { 'name': this.eventInfo.name, 'date': this.eventInfo.date, 'location': this.eventInfo.location, 'currency': this.eventInfo.currency };
|
||||
cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] = cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] ? cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] : {};
|
||||
|
||||
let data = cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] ? cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] : {};
|
||||
|
||||
let seat = JSON.parse( sessionStorage.getItem( 'tempStorage' ) );
|
||||
|
||||
let ticket = this.seating[ seat[ 1 ][ 1 ] ][ 'content' ][ seat[ 1 ][ 0 ] ];
|
||||
let ticketData = { 'name': ticket[ 'name' ], 'categoryID': ticketOption, 'category': this.eventInfo[ 'categories' ][ ticket[ 'category' ] ], 'price': this.eventInfo[ 'categories' ][ this.seating[ seat[ 1 ][ 1 ] ][ 'content' ][ seat[ 1 ][ 0 ] ][ 'category' ] ][ 'price' ][ ticketOption ], 'row':seat[ 1 ][ 1 ], 'seat':seat[ 1 ][ 0 ], 'ageGroup': this.eventInfo[ 'ageGroups' ][ ticketOption ][ 'name' ] };
|
||||
data[ String( seat[ 1 ][ 1 ] ) + String( seat[ 1 ][ 0 ] ) ] = ticketData;
|
||||
|
||||
cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] = data;
|
||||
|
||||
|
||||
sessionStorage.setItem( 'cart', JSON.stringify( cart ) );
|
||||
$( '#overlay' ).hide( 200 );
|
||||
this.selectedSeats = cart;
|
||||
this.sumUp();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadPreviouslySelected();
|
||||
}
|
||||
}
|
||||
</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%;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
grid-area: sidebar;
|
||||
background-color: var( --accent-background );
|
||||
color: var( --secondary-color );
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.ticket {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var( --overlay-color );
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.popup {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
background-color: var( --background-color );
|
||||
height: 60%;
|
||||
width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
.popup-content-wrapper {
|
||||
display: flex;
|
||||
height: 90%;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.close-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.close-button {
|
||||
cursor: pointer;
|
||||
margin-right: 3vh;
|
||||
margin-top: 3vh;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: var( --accent-background );
|
||||
color: var( --secondary-color );
|
||||
font-weight: bold;
|
||||
font-size: 110%;
|
||||
border-radius: 20px;
|
||||
border-style: none;
|
||||
padding: 10px 40px;
|
||||
transition: 0.6s;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: var( --accent-background-hover );
|
||||
transition: 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.price-table {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
236
src/webapp/main/src/components/notifications/notifications.vue
Normal file
236
src/webapp/main/src/components/notifications/notifications.vue
Normal file
@@ -0,0 +1,236 @@
|
||||
<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 notifications[ id ];
|
||||
delete this.queue[ this.queue.findIndex( id ) ];
|
||||
} catch ( error ) {
|
||||
console.log( 'notification to be deleted is nonexistent or currently being displayed' );
|
||||
}
|
||||
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' ];
|
||||
this.queue.reverse();
|
||||
this.queue.pop();
|
||||
} else {
|
||||
this.messageType = 'hide';
|
||||
}
|
||||
}
|
||||
},
|
||||
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: 10;
|
||||
color: white;
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.topleft {
|
||||
top: 3vh;
|
||||
left: 0.5vw;
|
||||
}
|
||||
|
||||
.topright {
|
||||
top: 3vh;
|
||||
right: 0.5vw;
|
||||
}
|
||||
|
||||
.bottomright {
|
||||
bottom: 3vh;
|
||||
right: 0.5vw;
|
||||
}
|
||||
|
||||
.bottomleft {
|
||||
top: 3vh;
|
||||
right: 0.5vw;
|
||||
}
|
||||
|
||||
.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 {
|
||||
background-color: rgb(0, 0, 99);
|
||||
}
|
||||
|
||||
.progress-spinner {
|
||||
animation: spin 2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate( 0deg );
|
||||
}
|
||||
to {
|
||||
transform: rotate( 720deg );
|
||||
}
|
||||
}
|
||||
</style>
|
||||
203
src/webapp/main/src/components/notifications/popups.vue
Normal file
203
src/webapp/main/src/components/notifications/popups.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<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 style="width: 100%; margin-top: 3%;">
|
||||
<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 === 'settings'" class="options">
|
||||
<h3>{{ data.message }}</h3>
|
||||
<settings v-model:settings="data.options"></settings>
|
||||
<div style="width: 100%; margin-top: 3%;">
|
||||
<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 === 'confirm'" class="confirm options">
|
||||
<h3>{{ data.message }}</h3>
|
||||
<div style="width: 100%; margin-top: 3%;">
|
||||
<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 style="width: 100%; margin-top: 3%;">
|
||||
<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>
|
||||
</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: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closePopup( message ) {
|
||||
$( '#popup-backdrop' ).fadeOut( 300 );
|
||||
if ( message ) {
|
||||
this.$emit( 'data', { 'data': this.data.selected, 'status': message } );
|
||||
}
|
||||
},
|
||||
closePopupAdvanced ( message, data ) {
|
||||
this.data[ 'selected' ] = data;
|
||||
this.closePopup( message );
|
||||
},
|
||||
openPopup ( message, options, dataType, selected ) {
|
||||
let data = { 'message': message ? message : 'No message defined on method call!!', 'options': options ? options : { '1': { 'value': 'undefined', 'displayName': 'No options specified in call' } }, 'selected': selected ? selected : '' };
|
||||
this.data = data;
|
||||
this.contentType = dataType ? 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;
|
||||
}
|
||||
|
||||
.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 );
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
379
src/webapp/main/src/components/seatplan.vue
Normal file
379
src/webapp/main/src/components/seatplan.vue
Normal file
@@ -0,0 +1,379 @@
|
||||
<template>
|
||||
<div class="seatingWrapper">
|
||||
<div class="sidebar">
|
||||
<h2>{{ eventInfo.name }}</h2>
|
||||
<h3>{{ eventInfo.date }}</h3>
|
||||
<h3>{{ eventInfo.location }}</h3>
|
||||
<h3>Selected tickets</h3>
|
||||
<table class="price-table" v-for="event in selectedSeats">
|
||||
<tr v-if="Object.keys( event.selectedSeats ).length">
|
||||
<h4>{{ event.name }}</h4>
|
||||
</tr>
|
||||
<tr v-for="ticket in event.selectedSeats">
|
||||
<td>{{ ticket.name }} ({{ ticket.ageGroup }})</td>
|
||||
<td>{{ eventInfo[ 'currency' ] }} {{ ticket[ 'price' ] }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h3>Total: {{ eventInfo[ 'currency' ] }} {{ total }}</h3>
|
||||
<router-link to="/cart">To cart</router-link>
|
||||
</div>
|
||||
<div class="seatingPlan">
|
||||
<h3>Seating plan</h3>
|
||||
<p>{{ eventInfo.RoomName }}</p>
|
||||
<p class="stage" v-if="eventInfo.stage">Stage</p>
|
||||
<div class="seating">
|
||||
<table>
|
||||
<tr v-for="row in seating">
|
||||
<td>
|
||||
{{ row.name }}
|
||||
</td>
|
||||
<td v-for="place in row.content">
|
||||
<div :class="place.category" class="active" v-if="place.available" @click="selectSeat( place.id, row.id )">
|
||||
<div v-if="place.selected" :title="row.name + ', ' + place.name + ' is currently selected, click to deselect'">
|
||||
<span class="material-symbols-outlined">done</span>
|
||||
</div>
|
||||
<div v-else :title="row.name + ', ' + place.name + ' is not currently selected, click to select'">
|
||||
<span class="material-symbols-outlined">living</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="occupied">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay" id="overlay">
|
||||
<div class="popup">
|
||||
<div class="popup-content">
|
||||
<div class="close-container">
|
||||
<span class="material-symbols-outlined close-button" @click="closePopup()">close</span>
|
||||
</div>
|
||||
<div class="popup-content-wrapper">
|
||||
<h3>Choose a ticket option</h3>
|
||||
<ul v-for="group in eventInfo.ageGroups">
|
||||
<li @click="storeSeat( group.id )" class="option">{{ group.name }} <i v-if="group.age">(0 - 15.99 years)</i> - {{ eventInfo.currency }} {{ pricingCurrentlySelected[ group.id ] }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay" id="placeNotAvailable">
|
||||
<div class="popup">
|
||||
<div class="popup-content">
|
||||
<div class="close-container">
|
||||
<span class="material-symbols-outlined close-button" @click="closePlaceNotAvailablePopup()">close</span>
|
||||
</div>
|
||||
<div class="popup-content-wrapper">
|
||||
<h3>One or more seat(s) you have previously selected is/are no longer available!</h3>
|
||||
<p>Please select another one!</p>
|
||||
<button class="button" @click="closePlaceNotAvailablePopup()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'seatplan',
|
||||
props: {
|
||||
ticketID: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
seating: { 'r1': { 'name': 'Row 1', 'id': 'r1', 'content':{ 'S1':{ 'name': 'Seat 1', 'id': 'S1', 'available': true, 'selected': false, 'category':'1' } } }, 'r2': { 'name': 'Row 2', 'id': 'r2', 'content':{ 'S1':{ 'name': 'S1', 'id': 'S1', 'available': true, 'selected': false, 'category':'2' } } } },
|
||||
eventInfo: { 'name': 'TestEvent2', 'location': 'TestLocation2', 'date': 'TestDate2', 'RoomName': 'TestRoom2', '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', 'age': null } }, 'ageGroupCount': 2, 'stage': true, 'maxTickets': 2 },
|
||||
selectedSeats: {},
|
||||
pricingCurrentlySelected: {},
|
||||
total: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadPreviouslySelected () {
|
||||
/*
|
||||
This function is called whenever the data on the webpage is to be reloaded
|
||||
*/
|
||||
|
||||
// load data from cart and set up cart if not available
|
||||
let cart = sessionStorage.getItem( 'cart' ) ? JSON.parse( sessionStorage.getItem( 'cart' ) ) : {};
|
||||
cart[ this.ticketID ?? 'default' ] = cart[ this.ticketID ?? 'default' ] ? cart[ this.ticketID ?? 'default' ] : { 'name': this.eventInfo.name, 'date': this.eventInfo.date, 'location': this.eventInfo.location, 'currency': this.eventInfo.currency };
|
||||
cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] = cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] ? cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] : {};
|
||||
|
||||
let data = cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] ? cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] : {};
|
||||
|
||||
let showError = false
|
||||
for ( let i in data ) {
|
||||
if ( this.seating[ data[ i ][ 'row' ] ][ 'content' ][ data[ i ][ 'seat' ] ][ 'available' ] ) {
|
||||
this.seating[ data[ i ][ 'row' ] ][ 'content' ][ data[ i ][ 'seat' ] ][ 'selected' ] = true;
|
||||
} else {
|
||||
showError = true;
|
||||
delete data[ i ];
|
||||
}
|
||||
}
|
||||
|
||||
if ( showError ) {
|
||||
setTimeout( function () {
|
||||
$( '#placeNotAvailable' ).show( 200 );
|
||||
console.log( 'showing error message' );
|
||||
}, 500 );
|
||||
}
|
||||
|
||||
// check if no ticket selected and prevent writing if no ticket
|
||||
// selected to not show too many events
|
||||
let isEmpty = sessionStorage.getItem( 'selectedTicket' ) ? false : true;
|
||||
|
||||
if ( !isEmpty ) {
|
||||
cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] = data;
|
||||
sessionStorage.setItem( 'cart', JSON.stringify( cart ) );
|
||||
}
|
||||
|
||||
this.selectedSeats = cart;
|
||||
this.sumUp();
|
||||
},
|
||||
sumUp () {
|
||||
// This function calculates the total price of the tickets for this event.
|
||||
let cart = sessionStorage.getItem( 'cart' ) ? JSON.parse( sessionStorage.getItem( 'cart' ) ) : {};
|
||||
|
||||
let price = 0;
|
||||
for ( let i in cart ) {
|
||||
for ( let entry in cart[ i ][ 'selectedSeats' ] ) {
|
||||
price += parseInt( cart[ i ][ 'selectedSeats' ][ entry ][ 'price' ] );
|
||||
}
|
||||
}
|
||||
|
||||
let back = {};
|
||||
|
||||
back[ 'total' ] = price;
|
||||
back[ 'currency' ] = this.eventInfo.currency;
|
||||
|
||||
this.total = price;
|
||||
|
||||
|
||||
// check if no ticket selected and prevent writing if no ticket
|
||||
// selected to not show too many events
|
||||
let isEmpty = sessionStorage.getItem( 'selectedTicket' ) ? false : true;
|
||||
|
||||
if ( !isEmpty ) {
|
||||
sessionStorage.setItem( 'backend', JSON.stringify( back ) );
|
||||
sessionStorage.setItem( 'cart', JSON.stringify( cart ) );
|
||||
}
|
||||
},
|
||||
closePlaceNotAvailablePopup () {
|
||||
$( '#placeNotAvailable' ).hide( 300 );
|
||||
},
|
||||
selectSeat( placeID, rowID ) {
|
||||
/*
|
||||
This function allows the user to select a seat and deselect it, if it has previously
|
||||
been selected.
|
||||
*/
|
||||
sessionStorage.setItem( 'tempStorage', JSON.stringify( { 1:[ placeID, rowID ] } ) );
|
||||
let cart = sessionStorage.getItem( 'cart' ) ? JSON.parse( sessionStorage.getItem( 'cart' ) ) : {};
|
||||
cart[ this.ticketID ?? 'default' ] = cart[ this.ticketID ?? 'default' ] ? cart[ this.ticketID ?? 'default' ] : {};
|
||||
cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] = cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] ? cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] : {};
|
||||
|
||||
let data = cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] ? cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] : {};
|
||||
|
||||
let isDeleting = false;
|
||||
|
||||
for ( let i in data ) {
|
||||
if ( data[ i ][ 'seat' ] == placeID && data[ i ][ 'row' ] == rowID ) {
|
||||
delete data[ i ];
|
||||
isDeleting = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.seating[ rowID ][ 'content' ][ placeID ][ 'selected' ] = !isDeleting;
|
||||
|
||||
if ( isDeleting ) {
|
||||
cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] = data;
|
||||
sessionStorage.setItem( 'cart', JSON.stringify( cart ) );
|
||||
this.selectedSeats = cart;
|
||||
this.sumUp();
|
||||
} else {
|
||||
if ( this.eventInfo.ageGroupCount > 1 ) {
|
||||
$( '#overlay' ).show( 200 );
|
||||
} else {
|
||||
this.storeSeat( '1' );
|
||||
}
|
||||
}
|
||||
|
||||
this.pricingCurrentlySelected = this.eventInfo[ 'categories' ][ this.seating[ rowID ][ 'content' ][ placeID ][ 'category' ] ][ 'price' ];
|
||||
},
|
||||
closePopup () {
|
||||
// This function closes the popup and sets the seat to not selected
|
||||
$( '#overlay' ).hide( 200 );
|
||||
let seat = JSON.parse( sessionStorage.getItem( 'tempStorage' ) );
|
||||
this.seating[ seat[ 1 ][ 1 ] ][ 'content' ][ seat[ 1 ][ 0 ] ][ 'selected' ] = false;
|
||||
},
|
||||
storeSeat( ticketOption ) {
|
||||
/*
|
||||
This function stores a ticket into the event's selected seat sessionStorage.
|
||||
*/
|
||||
|
||||
let cart = sessionStorage.getItem( 'cart' ) ? JSON.parse( sessionStorage.getItem( 'cart' ) ) : {};
|
||||
cart[ this.ticketID ?? 'default' ] = cart[ this.ticketID ?? 'default' ] ? cart[ this.ticketID ?? 'default' ] : { 'name': this.eventInfo.name, 'date': this.eventInfo.date, 'location': this.eventInfo.location, 'currency': this.eventInfo.currency };
|
||||
cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] = cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] ? cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] : {};
|
||||
|
||||
let data = cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] ? cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] : {};
|
||||
|
||||
let seat = JSON.parse( sessionStorage.getItem( 'tempStorage' ) );
|
||||
|
||||
let ticket = this.seating[ seat[ 1 ][ 1 ] ][ 'content' ][ seat[ 1 ][ 0 ] ];
|
||||
let ticketData = { 'name': ticket[ 'name' ], 'categoryID': ticketOption, 'category': this.eventInfo[ 'categories' ][ ticket[ 'category' ] ], 'price': this.eventInfo[ 'categories' ][ this.seating[ seat[ 1 ][ 1 ] ][ 'content' ][ seat[ 1 ][ 0 ] ][ 'category' ] ][ 'price' ][ ticketOption ], 'row':seat[ 1 ][ 1 ], 'seat':seat[ 1 ][ 0 ], 'ageGroup': this.eventInfo[ 'ageGroups' ][ ticketOption ][ 'name' ] };
|
||||
data[ String( seat[ 1 ][ 1 ] ) + String( seat[ 1 ][ 0 ] ) ] = ticketData;
|
||||
|
||||
cart[ this.ticketID ?? 'default' ][ 'selectedSeats' ] = data;
|
||||
|
||||
|
||||
sessionStorage.setItem( 'cart', JSON.stringify( cart ) );
|
||||
$( '#overlay' ).hide( 200 );
|
||||
this.selectedSeats = cart;
|
||||
this.sumUp();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadPreviouslySelected();
|
||||
}
|
||||
}
|
||||
</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%;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
grid-area: sidebar;
|
||||
background-color: var( --accent-background );
|
||||
color: var( --secondary-color );
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.seatingPlan {
|
||||
grid-area: main;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: justify;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.active {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.occupied {
|
||||
background-color: var( --hover-color );
|
||||
padding: 0.4%;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var( --overlay-color );
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.popup {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
background-color: var( --background-color );
|
||||
height: 60%;
|
||||
width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
.popup-content-wrapper {
|
||||
display: flex;
|
||||
height: 90%;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.close-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.close-button {
|
||||
cursor: pointer;
|
||||
margin-right: 3vh;
|
||||
margin-top: 3vh;
|
||||
}
|
||||
|
||||
.option {
|
||||
list-style: none;
|
||||
padding: 7px 15px;
|
||||
border-radius: 10px;
|
||||
border-color: var( --primary-color );
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
margin: 3px 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stage {
|
||||
border-color: var( --primary-color );
|
||||
border-style: solid;
|
||||
width: 80%;
|
||||
height: 7%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: var( --accent-background );
|
||||
color: var( --secondary-color );
|
||||
font-weight: bold;
|
||||
font-size: 110%;
|
||||
border-radius: 20px;
|
||||
border-style: none;
|
||||
padding: 10px 40px;
|
||||
transition: 0.6s;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: var( --accent-background-hover );
|
||||
transition: 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.price-table {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
337
src/webapp/main/src/components/seatplan/editor/properties.vue
Normal file
337
src/webapp/main/src/components/seatplan/editor/properties.vue
Normal file
@@ -0,0 +1,337 @@
|
||||
<!--
|
||||
* 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>
|
||||
<select v-model="internal[ active ].seatCountingStartingPoint" @change="resubmit()">
|
||||
<option v-for="category in categories" :value="category.value">{{ category.name }}</option>
|
||||
</select>
|
||||
</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: {
|
||||
// TODO: Load categories from server
|
||||
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 ( value ) {
|
||||
this.loadInternal();
|
||||
},
|
||||
active ( value ) {
|
||||
this.loadInternal();
|
||||
},
|
||||
scaleFactor ( value ) {
|
||||
this.loadInternal();
|
||||
},
|
||||
historyPos ( value ) {
|
||||
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>
|
||||
397
src/webapp/main/src/components/seatplan/editor/window.vue
Normal file
397
src/webapp/main/src/components/seatplan/editor/window.vue
Normal file
@@ -0,0 +1,397 @@
|
||||
<!--
|
||||
* 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">
|
||||
<!-- TODO: Add additional div with v-if to check if a location has been selected and warn if not so. -->
|
||||
<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, 'seatNumberingPosition': 1, 'sector': 'A', 'text': { 'text': 'TestText', 'textSize': 20, 'colour': '#20FFFF' }, 'numberingDirection': 'left' } },
|
||||
available: { 'redo': false, 'undo': false },
|
||||
scaleFactor: 1,
|
||||
sizePoll: null,
|
||||
prevSize: { 'h': window.innerHeight, 'w': window.innerWidth },
|
||||
zoomFactor: 1,
|
||||
historyPos: 0,
|
||||
generalSettings: { 'namingScheme': 'numeric' },
|
||||
seatCountInfo: { 'data': {}, 'count': 0 },
|
||||
}
|
||||
},
|
||||
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 () {
|
||||
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 );
|
||||
}
|
||||
};
|
||||
|
||||
this.loadSeatplan();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
let supportedBrowser = [];
|
||||
this.save();
|
||||
// TODO: Add warning for untested browsers & suboptimal window sizes!
|
||||
},
|
||||
eventHandler ( e ) {
|
||||
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;
|
||||
/*
|
||||
Load seatplan
|
||||
*/
|
||||
// TODO: load from server
|
||||
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 () {
|
||||
// TODO: Save seat count and seat config to server as well
|
||||
let progressNotification = this.$refs.notification.createNotification( 'Saving as draft', 5, 'progress', 'normal' );
|
||||
sessionStorage.setItem( 'seatplan', JSON.stringify( this.scaleDown( this.draggables ) ) );
|
||||
this.$refs.notification.createNotification( 'Saved as draft', 5, 'ok', 'normal' );
|
||||
// TODO: Save to server and add warning if no component has a seat start point if any component is a seat component
|
||||
},
|
||||
deploy () {
|
||||
// TODO: Save to server
|
||||
this.$refs.notification.createNotification( 'Deploying...', 5, 'progress', 'normal' );
|
||||
this.$refs.notification.createNotification( 'Deployed successfully', 5, 'ok', 'normal' );
|
||||
},
|
||||
addNewElement () {
|
||||
// TODO: Check that this algorithm actually works in practice. If not, replace with one that
|
||||
// searches for the first available ID or uses a var to determine ID.
|
||||
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' };
|
||||
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[ 'data' ][ info.id ] = info.data;
|
||||
},
|
||||
getSeatCount () {
|
||||
this.seatCountInfo[ 'count' ] = document.getElementsByClassName( 'seats' ).length;
|
||||
},
|
||||
},
|
||||
created () {
|
||||
this.runHook();
|
||||
this.sizePoll = setInterval( this.eventHandler, 250 );
|
||||
},
|
||||
unmounted() {
|
||||
clearInterval( this.sizePoll );
|
||||
},
|
||||
}
|
||||
</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>
|
||||
@@ -0,0 +1,119 @@
|
||||
<!--
|
||||
* 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 ] = Math.floor( 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>
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<!--
|
||||
* 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': { '0': Math.floor( w / size ) }, 'id': this.id };
|
||||
for ( let row = 0; row < Math.floor( h / size ); row++ ) {
|
||||
this.seats[ row ] = {};
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<!--
|
||||
* 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 ] = 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>
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<!--
|
||||
* 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%;';
|
||||
} else if ( this.origin === 2 ) {
|
||||
this.style = 'border-style: none solid none none';
|
||||
this.circularStyle = 'top: 0; right: 0;';
|
||||
} else if ( this.origin === 3 ) {
|
||||
this.style = 'border-style: solid none none none';
|
||||
this.circularStyle = 'top: -100%; right: 0;';
|
||||
} else if ( this.origin === 4 ) {
|
||||
this.style = 'border-style: none none none solid';
|
||||
this.circularStyle = 'top: -100%; right: 100%;';
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
origin ( value ) {
|
||||
this.updateOrigin();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.updateOrigin();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<!--
|
||||
* 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: center;
|
||||
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%;';
|
||||
} else if ( this.origin === 2 ) {
|
||||
this.style = 'border-style: none solid none none';
|
||||
this.circularStyle = 'top: 0; right: 0;';
|
||||
} else if ( this.origin === 3 ) {
|
||||
this.style = 'border-style: solid none none none';
|
||||
this.circularStyle = 'top: -100%; right: 0;';
|
||||
} else if ( this.origin === 4 ) {
|
||||
this.style = 'border-style: none none none solid';
|
||||
this.circularStyle = 'top: -100%; right: 100%;';
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
origin ( value ) {
|
||||
this.updateOrigin();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.updateOrigin();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<!--
|
||||
* 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 ( value ) {
|
||||
this.updateStyle();
|
||||
},
|
||||
scaleFactor ( value ) {
|
||||
this.updateStyle();
|
||||
},
|
||||
colour ( value ) {
|
||||
this.updateStyle();
|
||||
},
|
||||
textSize ( value ) {
|
||||
this.updateStyle();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.updateStyle();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,128 @@
|
||||
<!--
|
||||
* 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'">close</span>
|
||||
<span class="material-symbols-outlined" :style="seat.scaling" v-else-if="seat.status == 'sel'"
|
||||
:title="seat.displayName + ', Selected'">done</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': true } }
|
||||
}
|
||||
},
|
||||
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 = {};
|
||||
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++ ) {
|
||||
this.seats[ row ][ n ] = { 'style': '', 'id': 'sec' + this.data.sector + 'r' + row + 's' + n, 'displayName': ( this.data.sectorCount > 1 ? 'Sector ' + this.data.sector + ', ' : '' ) + 'Row ' + ( row + 1 ) + ', Seat ' + ( n + 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}%; `;
|
||||
}
|
||||
}
|
||||
},
|
||||
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;
|
||||
}
|
||||
}
|
||||
},
|
||||
selectSeat ( row, seat ) {
|
||||
console.log( row + ' ' + seat );
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
scaleFactor() {
|
||||
this.setScaleFactor();
|
||||
},
|
||||
h() {
|
||||
this.calculateChairs();
|
||||
},
|
||||
w() {
|
||||
this.calculateChairs();
|
||||
},
|
||||
origin() {
|
||||
this.calculateChairs();
|
||||
},
|
||||
startingRow() {
|
||||
this.calculateChairs();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.calculateChairs();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<!--
|
||||
* 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,
|
||||
},
|
||||
origin: {
|
||||
type: Number,
|
||||
"default": 1,
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
"default": { 'sector': 'A', 'sectorCount': 1, 'unavailableSeats': { 'secAr0s0': true }, 'categoryInfo': { 'pricing': { 'adult': { 'displayName': 'Adults - CHF 20.-', 'value': 'adult', 'price': 20 }, 'child': { 'displayName': 'Child (0 - 15.99y) - CHF 15.-', 'value': 'child', '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 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++ ) {
|
||||
this.seats[ row ][ n ] = { 'style': '', 'id': 'sec' + this.data.sector + 'r' + row + 's' + n, 'displayName': ( this.data.sectorCount > 1 ? 'Sector ' + this.data.sector + ', ' : '' ) + 'Row ' + ( row + 1 ) + ', Seat ' + ( n + 1 ), 'status': 'av', 'row': row, 'seat': n };
|
||||
// TODO: apply style of category
|
||||
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.fg ? this.data.categoryInfo.color.fg : 'black' }; background-color: ${ this.data.categoryInfo.color.bg ? this.data.categoryInfo.color.bg : 'rgba( 0, 0, 0, 0 )' }`;
|
||||
}
|
||||
|
||||
if ( this.data.unavailableSeats ) {
|
||||
if ( this.data.unavailableSeats[ this.seats[ row ][ n ][ 'id' ] ] ) {
|
||||
this.seats[ row ][ n ][ 'status' ] = 'nav';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
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;
|
||||
}
|
||||
}
|
||||
},
|
||||
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();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.calculateChairs();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<!--
|
||||
* 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'">close</span>
|
||||
<span class="material-symbols-outlined" :style="seat.scaling" v-else-if="seat.status == 'sel'"
|
||||
:title="seat.displayName + ', Selected'">done</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': true } }
|
||||
}
|
||||
},
|
||||
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 = {};
|
||||
for ( let row = this.startingRow; row < count; row++ ) {
|
||||
let nn = 2 + ( row - 1 ) * 2;
|
||||
this.seats[ row ] = {};
|
||||
for ( let n = 0; n < nn; n++ ) {
|
||||
this.seats[ row ][ n ] = { 'style': '', 'id': 'sec' + this.data.sector + 'r' + row + 's' + n, 'displayName': ( this.data.sectorCount > 1 ? 'Sector ' + this.data.sector + ', ' : '' ) + 'Row ' + ( row + 1 ) + ', Seat ' + ( n + 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}%; `;
|
||||
}
|
||||
}
|
||||
},
|
||||
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;
|
||||
}
|
||||
}
|
||||
},
|
||||
selectSeat ( row, seat ) {
|
||||
console.log( row + ' ' + seat );
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
scaleFactor() {
|
||||
this.setScaleFactor();
|
||||
},
|
||||
h() {
|
||||
this.calculateChairs();
|
||||
},
|
||||
w() {
|
||||
this.calculateChairs();
|
||||
},
|
||||
origin() {
|
||||
this.calculateChairs();
|
||||
},
|
||||
startingRow() {
|
||||
this.calculateChairs();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.calculateChairs();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<!--
|
||||
* 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%;';
|
||||
} else if ( this.origin === 2 ) {
|
||||
this.style = 'border-style: none solid none none';
|
||||
this.circularStyle = 'top: 0; right: 0;';
|
||||
} else if ( this.origin === 3 ) {
|
||||
this.style = 'border-style: solid none none none';
|
||||
this.circularStyle = 'top: -100%; right: 0;';
|
||||
} else if ( this.origin === 4 ) {
|
||||
this.style = 'border-style: none none none solid';
|
||||
this.circularStyle = 'top: -100%; right: 100%;';
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
origin ( value ) {
|
||||
this.updateOrigin();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.updateOrigin();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<!--
|
||||
* 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: center;
|
||||
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%;';
|
||||
} else if ( this.origin === 2 ) {
|
||||
this.style = 'border-style: none solid none none';
|
||||
this.circularStyle = 'top: 0; right: 0;';
|
||||
} else if ( this.origin === 3 ) {
|
||||
this.style = 'border-style: solid none none none';
|
||||
this.circularStyle = 'top: -100%; right: 0;';
|
||||
} else if ( this.origin === 4 ) {
|
||||
this.style = 'border-style: none none none solid';
|
||||
this.circularStyle = 'top: -100%; right: 100%;';
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
origin ( value ) {
|
||||
this.updateOrigin();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.updateOrigin();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<!--
|
||||
* 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 ( value ) {
|
||||
this.updateStyle();
|
||||
},
|
||||
scaleFactor ( value ) {
|
||||
this.updateStyle();
|
||||
},
|
||||
colour ( value ) {
|
||||
this.updateStyle();
|
||||
},
|
||||
textSize ( value ) {
|
||||
this.updateStyle();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.updateStyle();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
314
src/webapp/main/src/components/seatplan/userApp/userWindow.vue
Normal file
314
src/webapp/main/src/components/seatplan/userApp/userWindow.vue
Normal file
@@ -0,0 +1,314 @@
|
||||
<!--
|
||||
* 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">
|
||||
<h2>Seat plan: {{ event.name }}</h2>
|
||||
<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 :ref="'component' + draggable.id" v-if="draggable.shape == 'circular' && draggable.type == 'seat'" :scale-factor="scaleFactor"
|
||||
:w="draggable.w" :h="draggable.h" :origin="draggable.origin" :starting-row="draggable.startingRow"
|
||||
@seatSelected="( seat ) => { seatSelected( seat ) }"></circularSeatplanComponent>
|
||||
|
||||
<trapezoidSeatplanComponent :ref="'component' + draggable.id" 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"
|
||||
@seatSelected="( seat ) => { seatSelected( seat ) }"></trapezoidSeatplanComponent>
|
||||
|
||||
<rectangularSeatplanComponent :ref="'component' + draggable.id" v-else-if="draggable.shape == 'rectangular' && draggable.type == 'seat'" :scale-factor="scaleFactor"
|
||||
:w="draggable.w" :h="draggable.h" :origin="draggable.origin"
|
||||
@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"></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 id="cart"></sideCartView>
|
||||
<notifications ref="notification" location="topleft"></notifications>
|
||||
<popups ref="popups" size="normal" @data="data => { reserveTicket( 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 {
|
||||
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, 'seatCountingStartingPoint': 1, 'sector': 'A', 'text': { 'text': 'TestText', 'textSize': 20, 'colour': '#20FFFF' } }, 'ticketCount': 1 },
|
||||
event: { 'name': 'TestEvent2', 'location': 'TestLocation2', 'date': '2023-07-15', 'RoomName': 'TestRoom2', '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', 'age': null } }, 'ageGroupCount': 2, '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: {},
|
||||
}
|
||||
},
|
||||
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.loadSeatplan();
|
||||
sessionStorage.setItem( 'seatplan', JSON.stringify( this.scaleDown( this.draggables ) ) );
|
||||
// TODO: remove scaleDown function again once backend is up
|
||||
// TODO: Optimise for odd screen sizes and aspect ratios and fucking webkit
|
||||
},
|
||||
eventHandler ( e ) {
|
||||
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
|
||||
*/
|
||||
// TODO: load from server
|
||||
if ( sessionStorage.getItem( 'seatplan' ) ) {
|
||||
this.draggables = this.scaleUp( JSON.parse( sessionStorage.getItem( 'seatplan' ) ) );
|
||||
}
|
||||
|
||||
// TODO: Check if all seats are available
|
||||
let allSeatsAvailable = true;
|
||||
// Method: Server sends all user selected seats + all selected seats. If seat is in both
|
||||
// then selected, if just in all selected, taken, else available.
|
||||
|
||||
let self = this;
|
||||
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 );
|
||||
}
|
||||
},
|
||||
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 ) ) {
|
||||
|
||||
} 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' ] } );
|
||||
}
|
||||
},
|
||||
reserveTicket ( option ) {
|
||||
if ( option.status == 'ok' ) {
|
||||
this.$refs.component1[ 0 ].validateSeatSelection( this.selectedSeat, option.data );
|
||||
}
|
||||
// TODO: Make call to server to reserve ticket when data is returned & save to localStorage array.
|
||||
},
|
||||
seatDeselected ( seat ) {
|
||||
// TODO: Make call to server to deselect ticket & delete from localStorage array and delete eventArray if empty!
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.runHook();
|
||||
this.sizePoll = setInterval( this.eventHandler, 250 );
|
||||
},
|
||||
unmounted() {
|
||||
clearInterval( this.sizePoll );
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.parent {
|
||||
height: 80vh;
|
||||
aspect-ratio: 16 / 9;
|
||||
-webkit-aspect-ratio: 16 / 9;
|
||||
top: 17vh;
|
||||
left: 10vw;
|
||||
position: absolute;
|
||||
border: black 1px solid;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.draggable-box {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.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;
|
||||
position: fixed;
|
||||
top: 17vh;
|
||||
left: 10.5vw;
|
||||
}
|
||||
.toolbar button {
|
||||
margin-top: 10%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbar button:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
95
src/webapp/main/src/components/settings/rightClickMenu.vue
Normal file
95
src/webapp/main/src/components/settings/rightClickMenu.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<!--
|
||||
* 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>
|
||||
195
src/webapp/main/src/components/settings/settings.vue
Normal file
195
src/webapp/main/src/components/settings/settings.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<!--
|
||||
* 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 == '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 {
|
||||
props: {
|
||||
settings: Object,
|
||||
},
|
||||
methods: {
|
||||
showInfo ( box ) {
|
||||
$( '#' + box ).stop();
|
||||
$( '#' + box ).fadeIn( 300 );
|
||||
},
|
||||
hideInfo ( box ) {
|
||||
$( '#' + box ).stop();
|
||||
$( '#' + box ).fadeOut( 300 );
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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>
|
||||
53
src/webapp/main/src/components/sideCartView.vue
Normal file
53
src/webapp/main/src/components/sideCartView.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<!--
|
||||
* 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">
|
||||
<h2>Cart</h2>
|
||||
<div v-for="event in cart">
|
||||
<h3>{{ event.displayName }}</h3>
|
||||
<table>
|
||||
<tr v-for="ticket in event.tickets">
|
||||
<td>
|
||||
<h4>{{ ticket.displayName }}: </h4>
|
||||
</td>
|
||||
<td>
|
||||
{{ event.currency }} {{ ticket.price }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'sideCartView',
|
||||
props: {
|
||||
'cart': {
|
||||
type: Object,
|
||||
default: { 'TestEvent2': { 'displayName': 'TestEvent2', 'tickets': { 'secAr1s1': { 'displayName': 'Row 1, Seat 1', 'price': 20 } }, 'currency': 'CHF' } },
|
||||
total: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
cart() {
|
||||
this.calculateTotal();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
calculateTotal () {
|
||||
console.log( 'cart updated' );
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.calculateTotal();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
42
src/webapp/main/src/main.js
Normal file
42
src/webapp/main/src/main.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 = false;
|
||||
|
||||
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 );
|
||||
|
||||
app.use( router );
|
||||
app.mount( '#app' );
|
||||
} );
|
||||
} );
|
||||
localStorage.setItem( 'url', '' );
|
||||
} else {
|
||||
localStorage.setItem( 'url', 'http://localhost:8081' );
|
||||
userStore.setUserAuth( true );
|
||||
userStore.setAdminAuth( true );
|
||||
app.use( router );
|
||||
app.mount( '#app' );
|
||||
}
|
||||
93
src/webapp/main/src/router/adminRoutes.js
Normal file
93
src/webapp/main/src/router/adminRoutes.js
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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 - libreevent',
|
||||
adminAuthRequired: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'adminHome',
|
||||
component: () => import( '../views/admin/HomeView.vue' ),
|
||||
meta: {
|
||||
title: 'Home :: Admin - libreevent',
|
||||
adminAuthRequired: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'locations',
|
||||
name: 'adminLocations',
|
||||
component: () => import( '../views/admin/LocationsView.vue' ),
|
||||
meta: {
|
||||
title: 'Accounts :: Admin - libreevent',
|
||||
adminAuthRequired: true,
|
||||
permissions: 'root'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'pages',
|
||||
name: 'adminPages',
|
||||
component: () => import( '../views/admin/PagesView.vue' ),
|
||||
meta: {
|
||||
title: 'Pages :: Admin - libreevent',
|
||||
adminAuthRequired: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'events',
|
||||
name: 'adminEvents',
|
||||
component: () => import( '../views/admin/EventsView.vue' ),
|
||||
meta: {
|
||||
title: 'Events :: Admin - libreevent',
|
||||
adminAuthRequired: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'plugins',
|
||||
name: 'adminPlugins',
|
||||
component: () => import( '../views/admin/PluginsView.vue' ),
|
||||
meta: {
|
||||
title: 'Plugins :: Admin - libreevent',
|
||||
adminAuthRequired: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'adminSettings',
|
||||
component: () => import( '../views/admin/SettingsView.vue' ),
|
||||
meta: {
|
||||
title: 'Admin - libreevent',
|
||||
adminAuthRequired: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'events/view',
|
||||
name: 'eventDetails',
|
||||
component: () => import( '../views/admin/events/EventsDetailsView.vue' ),
|
||||
meta: {
|
||||
title: 'Event details :: Admin - libreevent',
|
||||
adminAuthRequired: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'events/analytics',
|
||||
name: 'eventAnalytics',
|
||||
component: () => import( '../views/admin/events/AnalyticsView.vue' ),
|
||||
meta: {
|
||||
title: 'Event analytics :: Admin - libreevent',
|
||||
adminAuthRequired: true,
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
66
src/webapp/main/src/router/index.js
Normal file
66
src/webapp/main/src/router/index.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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 { useBackendStore } from '@/stores/backendStore';
|
||||
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, from ) => {
|
||||
document.title = to.meta.title ? to.meta.title : 'libreevent';
|
||||
} );
|
||||
|
||||
let UserAccountPages = [ 'account' ];
|
||||
|
||||
let authRequired = false;
|
||||
|
||||
router.beforeEach( ( to, from ) => {
|
||||
let userStore = useUserStore();
|
||||
let backendStore = useBackendStore();
|
||||
backendStore.loadVisitedSetupPages();
|
||||
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 ( !isUserAuthenticated && to.name === 'pay' ) {
|
||||
return { name: 'purchase' };
|
||||
} else 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' };
|
||||
} else if ( to.name === '2fa' && !userStore.getUserTwoFACompliant ) {
|
||||
return { name: 'login' };
|
||||
} else if ( to.name === 'Admin2fa' && !userStore.getAdminTwoFACompliant ) {
|
||||
return { name: 'adminLogin' };
|
||||
}
|
||||
} );
|
||||
|
||||
|
||||
export default router;
|
||||
155
src/webapp/main/src/router/mainRoutes.js
Normal file
155
src/webapp/main/src/router/mainRoutes.js
Normal file
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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 - libreevent'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/tickets',
|
||||
name: 'tickets',
|
||||
component: () => import( '../views/purchasing/OrderView.vue' ),
|
||||
meta: {
|
||||
title: 'Order ticket - libreevent'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import( '../views/user/LoginView.vue' ),
|
||||
meta: {
|
||||
title: 'Login - libreevent'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/admin/login',
|
||||
name: 'adminLogin',
|
||||
component: () => import( '../views/admin/AdminLoginView.vue' ),
|
||||
meta: {
|
||||
title: 'Login :: Admin - libreevent'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/admin/twoFactors',
|
||||
name: 'admin2FA',
|
||||
component: () => import( '../views/admin/TwoFA.vue' ),
|
||||
meta: {
|
||||
title: 'Two Factor Authentication :: Admin - libreevent'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/signup',
|
||||
name: 'signup',
|
||||
component: () => import( '../views/user/SignupView.vue' ),
|
||||
meta: {
|
||||
title: 'Signup - libreevent'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/account',
|
||||
name: 'account',
|
||||
component: () => import( '../views/user/AccountView.vue' ),
|
||||
meta: {
|
||||
title: 'Account - libreevent'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/twoFactors',
|
||||
name: '2fa',
|
||||
component: () => import( '../views/user/TwoFA.vue' ),
|
||||
meta: {
|
||||
title: 'Two Factor Authentication - libreevent'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/tickets/details',
|
||||
name: 'ticketDetails',
|
||||
component: () => import( '../views/purchasing/TicketsDetailsView.vue' ),
|
||||
meta: {
|
||||
title: 'Details - libreevent',
|
||||
transition: 'scale'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/tickets/order',
|
||||
name: 'ticketOrder',
|
||||
component: () => import( '../views/purchasing/TicketsOrderingView.vue' ),
|
||||
meta: {
|
||||
title: 'Order ticket - libreevent',
|
||||
transition: 'scale'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/cart',
|
||||
name: 'cart',
|
||||
component: () => import( '../views/purchasing/CartView.vue' ),
|
||||
meta: {
|
||||
title: 'Cart - libreevent',
|
||||
transition: 'scale'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/purchase',
|
||||
name: 'purchase',
|
||||
component: () => import( '@/views/purchasing/PurchaseView.vue' ),
|
||||
meta: {
|
||||
title: 'Purchase - libreevent',
|
||||
transition: 'scale'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/pay',
|
||||
name: 'pay',
|
||||
component: () => import( '@/views/purchasing/PaymentView.vue' ),
|
||||
meta: {
|
||||
title: 'Pay - libreevent',
|
||||
transition: 'scale',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/admin/seatplan',
|
||||
name: 'adminSeatplanEditor',
|
||||
component: () => import( '@/views/admin/events/EditorView.vue' ),
|
||||
meta: {
|
||||
title: 'Seatplan Editor :: Admin - libreevent',
|
||||
adminAuthRequired: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/admin/ticketEditor',
|
||||
name: 'adminTicketEditor',
|
||||
component: () => import( '@/views/admin/events/TicketEditorView.vue' ),
|
||||
meta: {
|
||||
title: 'Ticket Editor :: Admin - libreevent',
|
||||
adminAuthRequired: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import( '@/views/404.vue' ),
|
||||
meta: {
|
||||
title: '404 - Page not found :: libreevent',
|
||||
transition: 'scale',
|
||||
}
|
||||
},
|
||||
]
|
||||
28
src/webapp/main/src/stores/backendStore.js
Normal file
28
src/webapp/main/src/stores/backendStore.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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': {}, 'guestPurchase': false, 'guestPurchaseAllowed': true } ),
|
||||
getters: {
|
||||
getVisitedSetupPages: ( state ) => state.visitedSetupPages,
|
||||
getIsGuestPurchase: ( state ) => state.guestPurchase,
|
||||
getIsGuestPurchaseAllowed: ( state ) => state.guestPurchaseAllowed,
|
||||
},
|
||||
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' ) ) : {};
|
||||
}
|
||||
}
|
||||
} );
|
||||
34
src/webapp/main/src/stores/userStore.js
Normal file
34
src/webapp/main/src/stores/userStore.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 } ),
|
||||
getters: {
|
||||
getUserAuthenticated: ( state ) => state.isUserAuth,
|
||||
getAdminAuthenticated: ( state ) => state.isAdminAuth,
|
||||
getUserTwoFACompliant: ( state ) => state.isTwoFACompliantUser,
|
||||
getAdminTwoFACompliant: ( state ) => state.isTwoFACompliantAdmin,
|
||||
},
|
||||
actions: {
|
||||
setUserAuth ( auth ) {
|
||||
this.isUserAuth = auth;
|
||||
},
|
||||
setAdminAuth ( auth ) {
|
||||
this.isAdminAuth = auth;
|
||||
},
|
||||
setUser2fa ( auth ) {
|
||||
this.isTwoFACompliantUser = auth;
|
||||
},
|
||||
setAdmin2fa ( auth ) {
|
||||
this.isTwoFACompliantAdmin = auth;
|
||||
}
|
||||
}
|
||||
} );
|
||||
48
src/webapp/main/src/views/404.vue
Normal file
48
src/webapp/main/src/views/404.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<!--
|
||||
* 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>
|
||||
29
src/webapp/main/src/views/HomeView.vue
Normal file
29
src/webapp/main/src/views/HomeView.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<!--
|
||||
* 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>
|
||||
126
src/webapp/main/src/views/admin/AdminLoginView.vue
Normal file
126
src/webapp/main/src/views/admin/AdminLoginView.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<!--
|
||||
* 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( sessionStorage.getItem( 'redirect' ) ? sessionStorage.getItem( 'redirect' ) : '/account' );
|
||||
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>
|
||||
|
||||
/* TODO: Update colour to image */
|
||||
.login {
|
||||
background-color: green;
|
||||
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%;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
#missing-email, #missing-password, #credentials-wrong {
|
||||
display: none;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
254
src/webapp/main/src/views/admin/AdminView.vue
Normal file
254
src/webapp/main/src/views/admin/AdminView.vue
Normal file
@@ -0,0 +1,254 @@
|
||||
<!--
|
||||
* 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' );">☰</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, your terms of service, etc.">Pages</router-link>
|
||||
<router-link to="/admin/events" class="admin-menu" @click="navMenu( 'hide' )" title="Change, view and analyse 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="Install, Uninstall and 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: 30vw;
|
||||
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: 1299px) {
|
||||
.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 ( 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>
|
||||
|
||||
127
src/webapp/main/src/views/admin/EventsView.vue
Normal file
127
src/webapp/main/src/views/admin/EventsView.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<!--
|
||||
* 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>
|
||||
<div class="order-app" v-if="events">
|
||||
<ul v-for="timeframe in eventList">
|
||||
<h3>{{ timeframe.name }}</h3>
|
||||
<li v-for="event in timeframe.content">
|
||||
<router-link to="/admin/events/view" class="ticket" @click="setActiveTicket( event.eventID );" v-if="new Date( event.date ).getTime() > currentDate">
|
||||
<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>
|
||||
<router-link to="/admin/events/analytics" class="ticket" @click="setActiveTicket( event.eventID );" v-else="new Date( event.date ).getTime() > currentDate">
|
||||
<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 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;
|
||||
}
|
||||
|
||||
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>
|
||||
export default {
|
||||
name: 'OrderView',
|
||||
methods: {
|
||||
setActiveTicket ( id ) {
|
||||
sessionStorage.setItem( 'selectedTicket', id );
|
||||
}
|
||||
},
|
||||
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-07-15', '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-06-13', 'startingPrice':15, 'location': 'TestLocation', 'eventID': 'test2', 'currency': 'CHF', 'logo': new URL( '/src/assets/logo.png', import.meta.url ).href } },
|
||||
currentDate: new Date().getTime(),
|
||||
eventList: { 'upcoming': { 'name': 'Upcoming', 'content': {} }, 'past': { 'name': 'Past', 'content': {} } },
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// Sort events object such that events closest to today are displayed first and past events displayed last
|
||||
let sortable = [];
|
||||
for ( let event in this.events ) {
|
||||
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 ( 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 ] ];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
53
src/webapp/main/src/views/admin/HomeView.vue
Normal file
53
src/webapp/main/src/views/admin/HomeView.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<!--
|
||||
* 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: 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>
|
||||
152
src/webapp/main/src/views/admin/LocationsView.vue
Normal file
152
src/webapp/main/src/views/admin/LocationsView.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<!--
|
||||
* 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.</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">
|
||||
<div class="location-name">
|
||||
<h3>{{ 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"></popups>
|
||||
<popups ref="popup2" size="huge"></popups>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import popups from '@/components/notifications/popups.vue';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
locations: { 'test':{ 'name':'TestLocation', 'locationID':'test', 'seatplan-enabled': true, 'seatplan': {} } },
|
||||
}
|
||||
},
|
||||
components: {
|
||||
popups,
|
||||
},
|
||||
methods: {
|
||||
selectLocation ( locationID ) {
|
||||
sessionStorage.setItem( 'locationID', locationID );
|
||||
this.$refs.popup.openPopup( 'Settings for ' + this.locations[ locationID ][ 'name' ], {
|
||||
'locationName': {
|
||||
'display': 'Location name',
|
||||
'id': 'locationName',
|
||||
'tooltip':'Give the location the event takes place a name. This name will also be shown to customers',
|
||||
'value': '',
|
||||
'type': 'text',
|
||||
},
|
||||
'usesSeatplan': {
|
||||
'display': 'Use seat plan?',
|
||||
'id': 'usesSeatplan',
|
||||
'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' );
|
||||
},
|
||||
addLocation () {
|
||||
this.$refs.popup.openPopup( 'Add a new location', {
|
||||
'locationName': {
|
||||
'display': 'Location name',
|
||||
'id': 'locationName',
|
||||
'tooltip':'Give the location the event takes place a name. This name will also be shown to customers',
|
||||
'value': '',
|
||||
'type': 'text',
|
||||
},
|
||||
'usesSeatplan': {
|
||||
'display': 'Use seat plan?',
|
||||
'id': 'usesSeatplan',
|
||||
'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' );
|
||||
},
|
||||
}
|
||||
};
|
||||
</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%;
|
||||
}
|
||||
</style>
|
||||
30
src/webapp/main/src/views/admin/PagesView.vue
Normal file
30
src/webapp/main/src/views/admin/PagesView.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<!--
|
||||
* 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) and other pages</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
formData: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setup () {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
31
src/webapp/main/src/views/admin/PluginsView.vue
Normal file
31
src/webapp/main/src/views/admin/PluginsView.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<!--
|
||||
* 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 and install more</p>
|
||||
<div class="bigButtons"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
formData: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setup () {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
259
src/webapp/main/src/views/admin/SettingsView.vue
Normal file
259
src/webapp/main/src/views/admin/SettingsView.vue
Normal file
@@ -0,0 +1,259 @@
|
||||
<!--
|
||||
* 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>Settings</h2>
|
||||
<settings v-model:settings="settings"></settings>
|
||||
<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>
|
||||
<p>Detailed explanation of payment gateways can be found <a href="https://libreevent.janishutz.com/docs/payments" target="_blank">here</a>. You may install more payment gateway integrations in the plugins section.</p>
|
||||
|
||||
<div class="admin-settings">
|
||||
<h2>Admin Accounts</h2>
|
||||
<p>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>.
|
||||
<br>Usually, the permissions automatically set by the system on account creation should be appropriate.</p>
|
||||
<div v-for="account in adminAccounts" class="account" @click="showAccountSettings( account.username );" title="Edit permissions of this account (right click for more options)" @contextmenu="( e ) => { e.preventDefault(); openRightClickMenu( account.username, e ); }">
|
||||
<div class="location-name">
|
||||
<h3>{{ account.username }}</h3>
|
||||
<p>{{ account.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<rightClickMenu ref="rclk" @command="( command ) => { executeCommand( command ) }"></rightClickMenu>
|
||||
<popups ref="popup" size="big"></popups>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import settings from '@/components/settings/settings.vue';
|
||||
import popups from '@/components/notifications/popups.vue';
|
||||
import rightClickMenu from '@/components/settings/rightClickMenu.vue';
|
||||
|
||||
export default {
|
||||
name: 'adminSettingsView',
|
||||
components: {
|
||||
settings,
|
||||
popups,
|
||||
rightClickMenu,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
adminAccounts: { 'janis': { 'username': 'janis', 'email': 'info@janishutz.com', 'permissions': [ ] }, 'admin': { 'username': 'admin', 'email': 'development@janishutz.com', 'permissions': [ ] } },
|
||||
currentlyOpenMenu: '',
|
||||
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': 'always',
|
||||
'type': 'select',
|
||||
'restrictions': {
|
||||
'always': {
|
||||
'displayName':'Always require',
|
||||
'value': 'always'
|
||||
},
|
||||
'userDecided': {
|
||||
'displayName':'User can decide',
|
||||
'value': 'userDecided'
|
||||
},
|
||||
'never': {
|
||||
'displayName':'Disable',
|
||||
'value': 'never'
|
||||
},
|
||||
}
|
||||
},
|
||||
'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 address?',
|
||||
'id': 'phoneNumberRequired',
|
||||
'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',
|
||||
},
|
||||
'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': {
|
||||
'stripe': {
|
||||
'displayName':'Stripe',
|
||||
'value': 'stripe'
|
||||
},
|
||||
'adyen': {
|
||||
'displayName':'Adyen',
|
||||
'value': 'adyen'
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showAccountSettings ( account ) {
|
||||
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',
|
||||
},
|
||||
'entryControl': {
|
||||
'display': 'Entry control',
|
||||
'id': 'entryControl',
|
||||
'tooltip':'Change this setting to allow or disallow the selected user to execute entry control at the entrance to your event location.',
|
||||
'value': true,
|
||||
'type': 'toggle',
|
||||
},
|
||||
}
|
||||
, 'settings' );
|
||||
},
|
||||
showPaymentSettings () {
|
||||
this.$refs.popup.openPopup( 'Payment gateway settings', {
|
||||
'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',
|
||||
},
|
||||
'entryControl': {
|
||||
'display': 'Entry control',
|
||||
'id': 'entryControl',
|
||||
'tooltip':'Change this setting to allow or disallow the selected user to execute entry control at the entrance to your event location.',
|
||||
'value': true,
|
||||
'type': 'toggle',
|
||||
},
|
||||
}
|
||||
, 'settings' );
|
||||
},
|
||||
executeCommand( command ) {
|
||||
if ( command === 'openPermissions' ) {
|
||||
this.showAccountSettings( this.currentlyOpenMenu );
|
||||
} else if ( command === 'deleteUser' ) {
|
||||
this.$refs.popup.openPopup( 'Do you really want to delete the user ' + this.currentlyOpenMenu + '?', {}, 'confirm' );
|
||||
}
|
||||
},
|
||||
handlePopupReturns( message, data ) {
|
||||
|
||||
},
|
||||
openRightClickMenu( id, event ) {
|
||||
this.$refs.rclk.openRightClickMenu( event, { 'edit': { 'command': 'openPermissions', 'symbol': 'edit', 'display': 'Edit permissions' }, 'delete': { 'command': 'deleteUser', 'symbol': 'delete', 'display': 'Delete User' } } )
|
||||
this.currentlyOpenMenu = id;
|
||||
}
|
||||
}
|
||||
};
|
||||
// TODO: Load gateways and settings in general from server.
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.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>
|
||||
110
src/webapp/main/src/views/admin/TwoFA.vue
Normal file
110
src/webapp/main/src/views/admin/TwoFA.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<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 ( !!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 = e => {
|
||||
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 );
|
||||
}
|
||||
let code = sessionStorage.getItem( '2faCode' ) ? sessionStorage.getItem( '2faCode' ) : '';
|
||||
this.code = { '1': code.slice( 0, 3 ), '2': code.substring( 3 ) };
|
||||
},
|
||||
}
|
||||
</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>
|
||||
17
src/webapp/main/src/views/admin/events/AnalyticsView.vue
Normal file
17
src/webapp/main/src/views/admin/events/AnalyticsView.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<!--
|
||||
* 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>
|
||||
<!-- TODO: Add toggle for real-time update (gets live updates from sse) -->
|
||||
<!-- Use chart.js for visualisations -->
|
||||
</div>
|
||||
</template>
|
||||
41
src/webapp/main/src/views/admin/events/EditorView.vue
Normal file
41
src/webapp/main/src/views/admin/events/EditorView.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<!--
|
||||
* 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</h2>
|
||||
<window />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import window from '@/components/seatplan/editor/window.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
window,
|
||||
},
|
||||
methods: {
|
||||
checkLocationSelected () {
|
||||
if ( !sessionStorage.getItem( 'locationID' ) ) {
|
||||
this.$router.push( '/admin/locations' );
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.checkLocationSelected();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
nav {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
192
src/webapp/main/src/views/admin/events/EventsDetailsView.vue
Normal file
192
src/webapp/main/src/views/admin/events/EventsDetailsView.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<!--
|
||||
* 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>
|
||||
<div class="category-wrapper">
|
||||
<p>Event Description</p>
|
||||
<textarea v-model="event.description" class="big-text" cols="70" rows="3" placeholder="Event description..."></textarea>
|
||||
<table class="category">
|
||||
<tr>
|
||||
<td>Event location</td>
|
||||
<td>
|
||||
<select v-model="event.location" class="small-text">
|
||||
<option value="TestLocation">TestLocation</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Event date</td>
|
||||
<td><input v-model="event.date" class="small-text" type="date"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ticket editor</td>
|
||||
<router-link to="/admin/ticketEditor">Edit ticket layout</router-link>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="ticket-settings">
|
||||
<h3>Ticket Settings</h3>
|
||||
<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 }}
|
||||
<tr v-for="price in category.price">
|
||||
<td>
|
||||
<div class="category-details">{{ price.name }}:</div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" v-model="price.price">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="category-details">Foreground colour:</div></td>
|
||||
<td>
|
||||
<input type="text" data-coloris v-model="category.fg" onkeydown="return false;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="category-details">Background colour:</div></td>
|
||||
<td>
|
||||
<input type="text" data-coloris v-model="category.bg" onkeydown="return false;">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="special-settings">
|
||||
<h3>General Settings</h3>
|
||||
<p>Currency codes used must be valid ISO 4217 codes. Read more on <a href="https://libreevent.janishutz.com/docs/admin-panel/events#currency">this page</a> of the documentation <!-- https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes"--></p>
|
||||
<settings v-model:settings="specialSettings"></settings>
|
||||
</div>
|
||||
<div>
|
||||
<p>Please read into 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">here</a> for more information</p>
|
||||
</div>
|
||||
<notifications ref="notification" location="topright"></notifications>
|
||||
</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%;;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import settings from '@/components/settings/settings.vue';
|
||||
import notifications from '@/components/notifications/notifications.vue';
|
||||
|
||||
export default {
|
||||
name: 'TicketsDetailsView',
|
||||
components: {
|
||||
settings,
|
||||
notifications,
|
||||
},
|
||||
created () {
|
||||
if ( !sessionStorage.getItem( 'selectedTicket' ) ) {
|
||||
this.$router.push( '/admin/events' );
|
||||
}
|
||||
this.eventID = sessionStorage.getItem( 'selectedTicket' );
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
event: { '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':'TestDate', 'startingPrice':15, 'location': 'TestLocation', 'eventID': 'test', 'currency': 'CHF', 'logo': 'logo.png', 'categories': { '1': { 'price': { '1': { 'price':25, 'name':'Child (0-15.99 years)'}, '2': { 'price':35, 'name':'Adult'} }, 'bg': 'black', 'fg': 'white', 'name': 'Category 1' }, '2': { 'price': { '1': { 'price':25, 'name':'Child (0-15.99 years)' }, '2': { 'price':35, 'name':'Adult'} }, 'bg': 'green', 'fg': 'white', 'name': 'Category 2' } } },
|
||||
specialSettings: {
|
||||
'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',
|
||||
},
|
||||
'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',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
136
src/webapp/main/src/views/admin/events/TicketEditorView.vue
Normal file
136
src/webapp/main/src/views/admin/events/TicketEditorView.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div id="ticketEditor">
|
||||
<h1>Ticket Editor</h1>
|
||||
<div class="toolbar">
|
||||
<router-link to="/admin/events/view" class="toolbar-item" title="Back to event settings">Back</router-link>
|
||||
<form class="toolbar-item">
|
||||
<input type="file" accept="application/pdf" id="pdfTemplate" class="toolbar-item">
|
||||
</form>
|
||||
<button @click="loadPDF();" class="toolbar-item">Load file</button>
|
||||
<button @click="saveTemplate()" class="toolbar-item" title="Save your ticket">Save Template</button>
|
||||
<button @click="testNotifications();" class="toolbar-item">Test notifications</button>
|
||||
</div>
|
||||
<div id="editor">Loading editor...</div>
|
||||
<notifications ref="notification" location="topleft"></notifications>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Designer, BLANK_PDF } from '@pdfme/ui';
|
||||
import notifications from '@/components/notifications/notifications.vue';
|
||||
|
||||
export default {
|
||||
name: 'ticketEditor',
|
||||
data() {
|
||||
return {
|
||||
designer: null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
notifications
|
||||
},
|
||||
methods: {
|
||||
saveTemplate() {
|
||||
// Save to server instead
|
||||
this.$refs.notification.createNotification( 'Saving...', 5, 'progress', 'normal' );
|
||||
this.$refs.notification.createNotification( 'Saved successfully', 5, 'ok', 'normal' );
|
||||
console.log( this.designer.getTemplate() );
|
||||
},
|
||||
testNotifications () {
|
||||
this.$refs.notification.createNotification( 'Warning', 10, 'warning', 'normal' );
|
||||
this.$refs.notification.createNotification( 'Error', 10, 'error', 'normal' );
|
||||
this.$refs.notification.createNotification( 'Info', 10, 'info', 'normal' );
|
||||
this.$refs.notification.createNotification( 'Ok', 10, 'ok', 'normal' );
|
||||
this.$refs.notification.createNotification( 'Progress', 10, 'progress', 'normal' );
|
||||
},
|
||||
loadPDF () {
|
||||
let files = document.getElementById( 'pdfTemplate' ).files;
|
||||
if ( files.length ) {
|
||||
let pdfLoading = this.$refs.notification.createNotification( 'Loading pdf...', 5, 'progress', 'normal' );
|
||||
let pdf = files[ 0 ];
|
||||
if ( pdf.name.substring( pdf.name.length - 4 ) === '.pdf' ) {
|
||||
let fileReader = new FileReader();
|
||||
let base64;
|
||||
let self = this;
|
||||
|
||||
// Onload of file read the file content
|
||||
// https://stackoverflow.com/questions/13538832/convert-pdf-to-a-base64-encoded-string-in-javascript?rq=4
|
||||
|
||||
fileReader.onload = function( fileLoadedEvent ) {
|
||||
base64 = fileLoadedEvent.target.result;
|
||||
let oldTemplate = self.designer.getTemplate();
|
||||
oldTemplate[ 'basePdf' ] = base64;
|
||||
self.designer.updateTemplate( oldTemplate );
|
||||
self.$refs.notification.cancelNotification( pdfLoading );
|
||||
self.$refs.notification.createNotification( 'Loaded pdf successfully', 5, 'ok', 'normal' );
|
||||
};
|
||||
fileReader.readAsDataURL( pdf );
|
||||
} else {
|
||||
this.$refs.notification.createNotification( 'Cannot load pdf: Not a pdf!', 5, 'error', 'normal' );
|
||||
}
|
||||
} else {
|
||||
this.$refs.notification.createNotification( 'Cannot load pdf: No file selected', 5, 'error', 'normal' );
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
setTimeout( () => {
|
||||
const domContainer = document.getElementById( 'editor' );
|
||||
// TODO: Load from server if available
|
||||
const template = {
|
||||
basePdf: BLANK_PDF,
|
||||
schemas: [
|
||||
{
|
||||
locationAndTime: {
|
||||
type: 'text',
|
||||
position: { x: 0, y: 0 },
|
||||
width: 10,
|
||||
height: 10,
|
||||
},
|
||||
ticketName: {
|
||||
type: 'text',
|
||||
position: { x: 10, y: 10 },
|
||||
width: 10,
|
||||
height: 10,
|
||||
},
|
||||
ticketQRCode: {
|
||||
type: 'qrcode',
|
||||
position: { x: 20, y: 20 },
|
||||
width: 10,
|
||||
height: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
this.designer = new Designer( { domContainer, template } );
|
||||
// designer.updateTemplate( ) -> Used to update the template AND base PDF
|
||||
}, 300 );
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
nav {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
#editor {
|
||||
height: 85vh;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
height: 5vh;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.toolbar-item {
|
||||
margin: 0;
|
||||
margin-left: 0.5%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<!--
|
||||
* 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>
|
||||
<div class="category-wrapper">
|
||||
<table class="category">
|
||||
<tr>
|
||||
<td>Location name</td>
|
||||
<td>
|
||||
<input type="text" v-model="event.name">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Seat plan editor</td>
|
||||
<router-link to="/admin/seatplan">Edit seat plan</router-link>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<button @click="openPopup()">Open Popup</button>
|
||||
<popups ref="popup" @data="( event ) => { popupReturnHandling( event ) }"></popups>
|
||||
<notifications ref="notification" location="topright"></notifications>
|
||||
</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%;;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import settings from '@/components/settings/settings.vue';
|
||||
import notifications from '@/components/notifications/notifications.vue';
|
||||
import popups from '@/components/notifications/popups.vue';
|
||||
|
||||
export default {
|
||||
name: 'TicketsDetailsView',
|
||||
components: {
|
||||
settings,
|
||||
notifications,
|
||||
popups,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
event: { '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':'TestDate', 'startingPrice':15, 'location': 'TestLocation', 'eventID': 'test', 'currency': 'CHF', 'logo': 'logo.png', 'categories': { '1': { 'price': { '1': { 'price':25, 'name':'Child (0-15.99 years)'}, '2': { 'price':35, 'name':'Adult'} }, 'bg': 'black', 'fg': 'white', 'name': 'Category 1' }, '2': { 'price': { '1': { 'price':25, 'name':'Child (0-15.99 years)' }, '2': { 'price':35, 'name':'Adult'} }, 'bg': 'green', 'fg': 'white', 'name': 'Category 2' } } },
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openPopup() {
|
||||
console.log( 'opening' );
|
||||
this.$refs.popup.openPopup();
|
||||
},
|
||||
popupReturnHandling( event ) {
|
||||
console.log( event );
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
29
src/webapp/main/src/views/admin/page/StartPageEditorView.vue
Normal file
29
src/webapp/main/src/views/admin/page/StartPageEditorView.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<!--
|
||||
* 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>
|
||||
158
src/webapp/main/src/views/purchasing/CartView.vue
Normal file
158
src/webapp/main/src/views/purchasing/CartView.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<!--
|
||||
* 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="cartNotEmpty" class="cart-list">
|
||||
<h3>Your tickets</h3>
|
||||
<ul v-for="event in tickets" class="cart-list">
|
||||
<li>{{ event.name }}
|
||||
<ul v-for="ticket in event.selectedSeats">
|
||||
<li>{{ ticket.name }} ({{ ticket.category.name }}, {{ ticket.ageGroup }}) {{ event.currency }} {{ ticket.price }} <span class="material-symbols-outlined deleteButton" @click="deleteEntry( ticket.name, event.name )" title="Delete ticket">delete</span></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tool-wrapper">
|
||||
<h4>Total: {{ backend.currency }} {{ backend.total }}</h4>
|
||||
<router-link to="/purchase">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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.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: 50%;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
tickets: {},
|
||||
backend: {},
|
||||
cartNotEmpty: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadCart () {
|
||||
this.cartNotEmpty = false;
|
||||
let tickets = JSON.parse( sessionStorage.getItem( 'cart' ) );
|
||||
|
||||
for ( let event in tickets ) {
|
||||
if ( Object.keys( tickets[ event ][ 'selectedSeats' ] ).length ) {
|
||||
this.cartNotEmpty = true;
|
||||
};
|
||||
}
|
||||
|
||||
this.tickets = tickets;
|
||||
this.backend = JSON.parse( sessionStorage.getItem( 'backend' ) );
|
||||
},
|
||||
deleteEntry( id, eventName ) {
|
||||
if ( confirm( 'Do you really want to delete this ticket?' ) ) {
|
||||
let tickets = JSON.parse( sessionStorage.getItem( 'cart' ) );
|
||||
for ( let event in tickets ) {
|
||||
let ev = tickets[ event ];
|
||||
if ( ev.name == eventName ) {
|
||||
for ( let ticket in ev[ 'selectedSeats' ] ) {
|
||||
if ( ev[ 'selectedSeats' ][ ticket ].name ) {
|
||||
delete tickets[ event ][ 'selectedSeats' ][ ticket ];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sessionStorage.setItem( 'cart', JSON.stringify( tickets ) );
|
||||
this.sumUp();
|
||||
this.loadCart();
|
||||
};
|
||||
},
|
||||
sumUp () {
|
||||
// This function calculates the total price of the tickets for this event.
|
||||
let cart = sessionStorage.getItem( 'cart' ) ? JSON.parse( sessionStorage.getItem( 'cart' ) ) : {};
|
||||
|
||||
let price = 0;
|
||||
for ( let i in cart ) {
|
||||
for ( let entry in cart[ i ][ 'selectedSeats' ] ) {
|
||||
price += parseInt( cart[ i ][ 'selectedSeats' ][ entry ][ 'price' ] );
|
||||
}
|
||||
}
|
||||
|
||||
let back = {};
|
||||
|
||||
back[ 'total' ] = price;
|
||||
back[ 'currency' ] = this.backend.currency;
|
||||
|
||||
sessionStorage.setItem( 'backend', JSON.stringify( back ) );
|
||||
|
||||
this.total = price;
|
||||
|
||||
sessionStorage.setItem( 'cart', JSON.stringify( cart ) );
|
||||
},
|
||||
},
|
||||
created () {
|
||||
this.loadCart();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style>
|
||||
nav {
|
||||
display: initial;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
<!--
|
||||
* libreevent - GuestPurchaseView.vue
|
||||
*
|
||||
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
|
||||
* https://janishutz.com, development@janishutz.com
|
||||
*
|
||||
*
|
||||
-->
|
||||
125
src/webapp/main/src/views/purchasing/OrderView.vue
Normal file
125
src/webapp/main/src/views/purchasing/OrderView.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<!--
|
||||
* 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="events">
|
||||
<ul>
|
||||
<li v-for="event in orderedEvents">
|
||||
<router-link to="/tickets/details" class="ticket" @click="setActiveTicket( event.eventID );">
|
||||
<div class="ticket-name">
|
||||
<h3>{{ event.name }}</h3>
|
||||
<p>{{ event.description }}</p>
|
||||
</div>
|
||||
<div class="ticket-info">
|
||||
<p>Free seats: {{ event.freeSeats }} / {{ event.maxSeats }}</p>
|
||||
<p>{{ event.location }}, {{ event.dateString }}</p>
|
||||
<h4>Starting at {{ event.currency }} {{ event.startingPrice }}</h4>
|
||||
</div>
|
||||
<img :src="event.logo" alt="event logo" class="ticket-logo">
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</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;
|
||||
}
|
||||
|
||||
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: 35%;
|
||||
}
|
||||
|
||||
.ticket-info {
|
||||
margin-left: auto;
|
||||
margin-right: auto
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'OrderView',
|
||||
methods: {
|
||||
setActiveTicket ( id ) {
|
||||
sessionStorage.setItem( 'selectedTicket', id );
|
||||
}
|
||||
},
|
||||
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-07-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-01T09: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()
|
||||
}
|
||||
},
|
||||
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>
|
||||
71
src/webapp/main/src/views/purchasing/PaymentView.vue
Normal file
71
src/webapp/main/src/views/purchasing/PaymentView.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<!--
|
||||
* libreevent - PaymentView.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>Purchase</h1>
|
||||
<h3>Please choose a payment option</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.order-app {
|
||||
text-align: justify;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.ticket-info {
|
||||
margin-left: auto;
|
||||
margin-right: auto
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<style>
|
||||
nav {
|
||||
display: initial;
|
||||
}
|
||||
</style>
|
||||
217
src/webapp/main/src/views/purchasing/PurchaseView.vue
Normal file
217
src/webapp/main/src/views/purchasing/PurchaseView.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<!--
|
||||
* 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">
|
||||
<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">
|
||||
<div class="data">
|
||||
<h2>Billing</h2>
|
||||
<table class="billing-info-table">
|
||||
<tr>
|
||||
<td>Street and house number</td>
|
||||
<td><input type="text" name="address" id="address"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<router-link to="/pay" id="buy-button">Buy now</router-link>
|
||||
</div>
|
||||
<div class="cart">
|
||||
<div class="cart-list">
|
||||
<h2>Order summary</h2>
|
||||
<h3>Your tickets</h3>
|
||||
<ul v-for="event in tickets">
|
||||
<li>{{ event.name }}
|
||||
<ul v-for="ticket in event.selectedSeats">
|
||||
<li>{{ ticket.name }} ({{ ticket.category.name }}, {{ ticket.ageGroup }}) {{ event.currency }} {{ ticket.price }}</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tool-wrapper wrapper-loggedIn">
|
||||
<h4>Total: {{ backend.currency }} {{ backend.total }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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%;
|
||||
}
|
||||
|
||||
#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%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
||||
.option-button {
|
||||
border-style: solid;
|
||||
border-color: var( --primary-color );
|
||||
border-radius: 20px;
|
||||
padding: 6% 7%;
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 0.5%;
|
||||
color: var( --primary-color );
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.data {
|
||||
grid-area: main;
|
||||
display: flex;
|
||||
justify-content: justify;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.option-button:hover {
|
||||
background-color: var( --hover-color );
|
||||
color: var( --secondary-color )
|
||||
}
|
||||
|
||||
.cart {
|
||||
grid-area: sidebar;
|
||||
background-color: var( --accent-background );
|
||||
color: var( --secondary-color );
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.wrapper-buttons {
|
||||
width: 40%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
height: 100%;
|
||||
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';
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.cart-list {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tool-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import { useBackendStore } from '@/stores/backendStore';
|
||||
import { mapStores } from 'pinia';
|
||||
|
||||
export default {
|
||||
name: 'PurchaseView',
|
||||
data () {
|
||||
return {
|
||||
settings: { 'accountRequired': true, 'requiresAddress': true, 'requiresAge': true, 'requiresSpecialNumber': true, 'specialNumberDisplayName': { 'de': '', 'en': 'id number' } },
|
||||
isAuthenticated: false,
|
||||
tickets: {},
|
||||
backend: {},
|
||||
cartNotEmpty: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores( useUserStore ),
|
||||
...mapStores( useBackendStore )
|
||||
},
|
||||
methods: {
|
||||
loadData () {
|
||||
this.cartNotEmpty = false;
|
||||
let tickets = JSON.parse( sessionStorage.getItem( 'cart' ) );
|
||||
|
||||
for ( let event in tickets ) {
|
||||
if ( Object.keys( tickets[ event ][ 'selectedSeats' ] ).length ) {
|
||||
this.cartNotEmpty = true;
|
||||
};
|
||||
}
|
||||
|
||||
if ( this.cartNotEmpty ) {
|
||||
this.tickets = tickets;
|
||||
this.backend = JSON.parse( sessionStorage.getItem( 'backend' ) );
|
||||
this.isAuthenticated = this.userStore.getUserAuthenticated;
|
||||
this.settings.accountRequired = !this.backendStore.getIsGuestPurchaseAllowed;
|
||||
} else {
|
||||
this.$router.push( '/tickets' );
|
||||
}
|
||||
},
|
||||
setRedirect () {
|
||||
sessionStorage.setItem( 'redirect', '/purchase' );
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.loadData();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
50
src/webapp/main/src/views/purchasing/TicketsDetailsView.vue
Normal file
50
src/webapp/main/src/views/purchasing/TicketsDetailsView.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<!--
|
||||
* 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">
|
||||
<h1>{{ event.name }}</h1>
|
||||
<router-link to="/tickets"><span class="material-symbols-outlined" style="font-size: 100%;">arrow_back</span>Back</router-link>
|
||||
<p>{{ event.description }}</p>
|
||||
<router-link to="/tickets/order">Order tickets</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.details {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.top-container {
|
||||
background: linear-gradient( to top, rgba( 0, 0, 0, 0.9 ) 50%, rgba( 0, 0, 0, 0 ) ), url( '@/assets/logo.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TicketsDetailsView',
|
||||
created () {
|
||||
if ( !sessionStorage.getItem( 'selectedTicket' ) ) {
|
||||
this.$router.push( '/tickets' );
|
||||
}
|
||||
this.eventID = sessionStorage.getItem( 'selectedTicket' );
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
event: { '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':'TestDate', 'startingPrice':15, 'location': 'TestLocation', 'eventID': 'test', 'currency': 'CHF', 'logo': 'logo.png' },
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
51
src/webapp/main/src/views/purchasing/TicketsOrderingView.vue
Normal file
51
src/webapp/main/src/views/purchasing/TicketsOrderingView.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<!--
|
||||
* 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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.details {
|
||||
flex-grow: 1;
|
||||
}
|
||||
</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: true,
|
||||
eventID: '',
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if ( !sessionStorage.getItem( 'selectedTicket' ) ) {
|
||||
this.$router.push( '/tickets' );
|
||||
}
|
||||
this.eventID = sessionStorage.getItem( 'selectedTicket' );
|
||||
if ( this.eventID == 'test' ) {
|
||||
this.hasSeatplan = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
13
src/webapp/main/src/views/user/AccountView.vue
Normal file
13
src/webapp/main/src/views/user/AccountView.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Account</h1>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style>
|
||||
nav {
|
||||
display: initial;
|
||||
}
|
||||
</style>
|
||||
127
src/webapp/main/src/views/user/LoginView.vue
Normal file
127
src/webapp/main/src/views/user/LoginView.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<!--
|
||||
* 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><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>
|
||||
<router-link to="/signup" class="button">Sign up instead</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' ) ? 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>
|
||||
|
||||
/* TODO: Update colour to image */
|
||||
.login {
|
||||
background-color: green;
|
||||
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%;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
#missing-email, #missing-password, #credentials-wrong {
|
||||
display: none;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
74
src/webapp/main/src/views/user/SignupView.vue
Normal file
74
src/webapp/main/src/views/user/SignupView.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<!--
|
||||
* 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>
|
||||
<form>
|
||||
<label for="mail">Email</label><br>
|
||||
<input type="email" v-model="formData[ 'mail' ]" name="mail" id="mail" required><br><br>
|
||||
<label for="name">Full name</label><br>
|
||||
<input type="text" v-model="formData[ 'name' ]" name="name" id="name" required><br><br>
|
||||
<label for="password">Password</label><br>
|
||||
<input type="password" v-model="formData[ 'password' ]" name="password" id="password" required><br><br>
|
||||
<label for="password2">Confirm password</label><br>
|
||||
<input type="password" v-model="formData[ 'password2' ]" name="password2" id="password2" required>
|
||||
</form>
|
||||
<button @click="signup();" class="button">Sign up</button>
|
||||
<router-link to="/login" class="button">Log in instead</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
formData: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
signup () {
|
||||
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login {
|
||||
background-color: green;
|
||||
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%;
|
||||
}
|
||||
</style>
|
||||
110
src/webapp/main/src/views/user/TwoFA.vue
Normal file
110
src/webapp/main/src/views/user/TwoFA.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<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': '' }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores( useUserStore ),
|
||||
},
|
||||
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' ) + '/user/2fa/check', { withCredentials: true } );
|
||||
|
||||
let self = this;
|
||||
|
||||
source.onmessage = ( e ) => {
|
||||
if ( e.data === 'authenticated' ) {
|
||||
self.userStore.setUserAuth( true );
|
||||
self.$router.push( '/account' );
|
||||
console.log( e.data );
|
||||
}
|
||||
}
|
||||
|
||||
source.onopen = e => {
|
||||
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 );
|
||||
}
|
||||
let code = sessionStorage.getItem( '2faCode' ) ? sessionStorage.getItem( '2faCode' ) : '';
|
||||
this.code = { '1': code.slice( 0, 3 ), '2': code.substring( 3 ) };
|
||||
},
|
||||
}
|
||||
</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>
|
||||
15
src/webapp/main/vite.config.js
Normal file
15
src/webapp/main/vite.config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
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': 8080
|
||||
}
|
||||
} );
|
||||
4
src/webapp/main/vue.config.js
Normal file
4
src/webapp/main/vue.config.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const { defineConfig } = require( '@vue/cli-service' );
|
||||
module.exports = defineConfig( {
|
||||
transpileDependencies: true
|
||||
} );
|
||||
Reference in New Issue
Block a user