Merge pull request #1 from simplePCBuilding/dev-V1-new

Merge development version into main branch
This commit is contained in:
Janis Hutz
2023-05-29 15:58:35 +00:00
committed by GitHub
133 changed files with 26097 additions and 2 deletions

24
.circleci/config.yml Normal file
View File

@@ -0,0 +1,24 @@
version: 2.1
orbs:
node: circleci/node@5.1.0
jobs:
build_and_test:
executor: node/default
steps:
- checkout
- node/install-packages:
pkg-manager: npm
- run:
command: npm run build
name: Build app
- run:
command: npm run test
name: Run tests
- run:
command: npm run build-website
name: Build documentation
- persist_to_workspace:
root: ~/project
paths:
- .

69
.eslintrc.js Normal file
View File

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

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
#
# libreevent - .gitignore
#
# Created by Janis Hutz 02/26/2023, Licensed under the GPL V3 License
# https://janishutz.com, development@janishutz.com
#
#
# ignore ALL .log files
*.log
# ignore node_modules (can be rebuilt with npm i --> shrinks repo size)
node_modules
# ignore dist folder (this repo only contains source code!)
/dist

View File

@@ -1,8 +1,62 @@
# myevent <div id="title" align="center">
<img src="./assets/logo.png" width="300">
<h1>libreevent</h1>
</div>
<div id="badges" align="center">
<img src="https://img.shields.io/github/release/simplePCBuilding/libreevent.svg">
<img src="https://img.shields.io/github/license/simplePCBuilding/libreevent.svg">
<img src="https://img.shields.io/github/repo-size/simplePCBuilding/libreevent.svg">
<img src="https://img.shields.io/tokei/lines/github/simplePCBuilding/libreevent">
<img src="https://img.shields.io/github/issues-pr-raw/simplePCBuilding/libreevent">
<img src="https://img.shields.io/github/languages/top/simplePCBuilding/libreevent">
<img src="https://img.shields.io/github/directory-file-count/simplePCBuilding/libreevent.svg">
<img src="https://img.shields.io/github/package-json/v/simplePCBuilding/libreevent.svg">
</div>
A fully featured, fully free and open source event management solution you can host yourself, to manage your event and sell tickets. All you need is a webserver that can run node.js! A fully featured, fully free and open source event management solution you can host yourself, to manage your event and sell tickets. All you need is a webserver that can run node.js!
Visit our [website](https://libreevent.janishutz.com)
# System requirements
- node.js V16.0+
- npm
- (OPTIONAL) MySQL
- any CPU from the last 10 years
- Any operating system that can run node.js
# Download
You may download this project using the GitHub releases page or the direct links on the [libreevent website](https://libreevent.janishutz.com/download) as this only downloads the ready-to-distribute version, not the development version.
Alternatively, you may download the project directly from GitHub (by cloning it or downloading the code) but you'll have to compile and package the project [manually](https://libreevent.janishutz.com/docs/contributing/packaging).
# Contributing
If you want to contribute to this project, please read more [here](https://libreevent.janishutz.com/docs/contributing). Until the end of October 2023, no contributions can be accepted into master.
# Supporting the project
If you like this project and it helped you save money, please consider donating to help fund the continuous development. If you are a company, please contact me [here](https://libreevent.janishutz.com/docs/sponsoring) if you want to sponsor the project and become an official partner.
# Repository structure
- [assets/](/assets/): contains the logo (as png and GIMP file), also iOS and Android marketing materials, just global assets (images / videos)
- [src/](/src/): contains all of the source code of the project:
- [src/apps](/src/apps/): contains the source code of the iOS and Android app.
- [src/server](/src/server/): contains the source code for the node.js application that runs on the server side.
- [src/webapp](/src/webapp/): contains the source code for the frontend, contains lots of vue files.
- [website/](/website/): contains all of the website files:
- [website/dist/](/website/dist/): contains all the ready to distribute website files
- [website/src/](/website/src/): contains all of the source files (markdown format) for the website. These files are converted into HTML by the build script.
- [package.js](/package.js): collects all of the files of the project and copies them into the [dist/](/dist/) folder. It also minifies the files in the process to reduce package size.
- [.eslintrc.js](/.eslintrc.js): ESlint config, the linter used for the project
- [.gitignore](/.gitignore): ignored files, currently is node_modules & log files.
You may notice some additional folders appearing after running
```
npm run package
```
This is to shrink the repository size. Distribution ready files can be found in the releases or on our [website](https://libreevent.janishutz.com/download).
## This project is currently NOT ready to be used! ## This project is currently NOT ready to be used!
Development of this project is currently ongoing and no stable version is available yet. Development of this project is currently ongoing and no stable version is available yet.
## ROADMAP ## ROADMAP
The goal is to get this tool fully functioning by the End of September 2023. The goal is to get this tool fully functioning by the End of September 2023.

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
assets/logo.xcf Normal file

Binary file not shown.

8
package.js Normal file
View File

@@ -0,0 +1,8 @@
/*
* libreevent - package.js
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/

24
package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "libreevent",
"version": "1.0.0",
"description": "A free and open source event management solution",
"main": "/dist/app.js",
"scripts": {
"test": "npm run test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/simplePCBuilding/libreevent.git"
},
"keywords": [
"eventmanager",
"events",
"tickets"
],
"author": "Janis Hutz",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/simplePCBuilding/libreevent/issues"
},
"homepage": "https://libreevent.janishutz.com"
}

View File

2
src/apps/iOS/README.md Normal file
View File

@@ -0,0 +1,2 @@
# iOS - App
This app has the same functionality as the Android App.

View File

@@ -0,0 +1,27 @@
/*
* libreevent - pwdmanager.js
*
* Created by Janis Hutz 03/26/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
/*
These functions are required to verify user login and to create new users
and to hash new passwords (if user changes password.) This here is only
used for the admin panel, another one is used for the normal user accounts
to separate the two for additional security.
*/
// import and init
const bcrypt = require( 'bcrypt' );
const db = require( '../backend/db/db.js' );
module.exports.checkpassword = function checkpassword ( username, password ) {
return new Promise( resolve => {
db.getData( 'admin', username ).then( data => {
resolve( bcrypt.compareSync( password, data ) );
} );
} );
};

View File

@@ -0,0 +1,45 @@
/*
* libreevent - routes.js (admin)
*
* Created by Janis Hutz 03/11/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
const pwdmanager = require( './pwdmanager.js' );
module.exports = ( app, settings ) => {
/*
Admin login route that checks the password
*/
app.post( '/admin/auth', ( request, response ) => {
pwdmanager.checkpassword( request.body.mail, request.body.pwd ).then( data => {
if ( data ) {
if ( settings.twoFA ) {
response.send( '2fa' );
} else {
request.session.loggedInAdmin = true;
response.send( 'ok' );
}
} else {
response.send( 'pwErr' );
}
} );
} );
app.get( '/test/login', ( request, response ) => {
request.session.loggedInAdmin = true;
response.send( 'Logged in' );
} );
app.get( '/admin/logout', ( request, response ) => {
request.session.loggedInAdmin = false;
response.send( 'logged out' );
} );
app.get( '/api/getAuth', ( request, response ) => {
response.send( { 'admin': request.session.loggedInAdmin ? true : false, 'user': request.session.loggedInUser ? true : false } );
} );
};

45
src/server/app.js Normal file
View File

@@ -0,0 +1,45 @@
/*
* libreevent - app.js
*
* Created by Janis Hutz 02/26/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
const express = require( 'express' );
let app = express();
const path = require( 'path' );
const expressSession = require( 'express-session' );
const bodyParser = require( 'body-parser' );
const cookieParser = require( 'cookie-parser' );
const http = require( 'http' );
const fs = require( 'fs' );
// const env = process.env.PROD || false;
const settings = JSON.parse( fs.readFileSync( path.join( __dirname + '/config.json' ) ) );
// initialise express with middlewares
app.use( expressSession( {
secret: 'gaoevgoawefgo083tq2rfvöfaf0p8',
resave: true,
saveUninitialized: true
} ) );
app.use( bodyParser.urlencoded( { extended: false } ) );
app.use( bodyParser.json() );
app.use( cookieParser() );
// app.use( favicon( path.join( __dirname + '/ui/assets/logo.png' ) ) );
app.use( express.static( '../webapp/dist' ) );
require( './admin/routes.js' )( app, settings ); // admin route
app.use( ( request, response ) => {
response.sendFile( path.join( __dirname + '/../webapp/dist/index.html' ) );
} );
const PORT = process.env.PORT || 8080;
http.createServer( app ).listen( PORT );

View File

@@ -0,0 +1,15 @@
/*
* libreevent - db.js
*
* Created by Janis Hutz 03/26/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
module.exports.getData = function getData ( db, searchQuery ) {
console.log( db + searchQuery );
return new Promise( resolve => {
resolve( '$2b$05$ElMYWoMjk7567lXkIkee.e.6cxCrWU4gkfuNLB8gmGYLQQPm7gT3O' );
} );
};

View File

View File

View File

3
src/server/config.json Normal file
View File

@@ -0,0 +1,3 @@
{
"init":true
}

0
src/server/mail.json Normal file
View File

3153
src/server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

70
src/server/package.json Normal file
View File

@@ -0,0 +1,70 @@
{
"name": "libreevent",
"version": "1.0.0",
"description": "Free & Open source event management solution",
"main": "app.js",
"directories": {
"doc": "docs"
},
"devDependencies": {
"acorn": "^8.8.2",
"buffer-from": "^1.1.2",
"camel-case": "^4.1.2",
"clean-css": "^5.3.2",
"commander": "^9.5.0",
"css-b64-images": "^0.2.5",
"debug": "^4.3.4",
"dot-case": "^3.0.4",
"entities": "^4.4.0",
"find-up": "^6.3.0",
"html-minifier-terser": "^7.1.0",
"jju": "^1.4.0",
"locate-path": "^7.2.0",
"lower-case": "^2.0.2",
"minify": "^9.2.0",
"ms": "^2.1.2",
"no-case": "^3.0.4",
"p-limit": "^4.0.0",
"p-locate": "^6.0.0",
"param-case": "^3.0.4",
"pascal-case": "^3.1.2",
"path-exists": "^5.0.0",
"readjson": "^2.2.2",
"relateurl": "^0.2.7",
"simport": "^1.2.0",
"source-map": "^0.6.1",
"source-map-support": "^0.5.21",
"terser": "^5.16.5",
"try-catch": "^3.0.1",
"try-to-catch": "^3.0.1",
"tslib": "^2.5.0",
"yocto-queue": "^1.0.0"
},
"dependencies": {
"bcrypt": "^5.1.0",
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"express": "^4.18.2",
"express-session": "^1.17.3",
"serve-favicon": "^2.5.0",
"serve-static": "^1.15.0"
},
"scripts": {
"test": "test.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/simplePCBuilding/libreevent.git"
},
"keywords": [
"event",
"management",
"solution"
],
"author": "Janis Hutz",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/simplePCBuilding/libreevent/issues"
},
"homepage": "https://libreevent.janishutz.com"
}

View File

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

23
src/webapp/.gitignore vendored Normal file
View 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/README.md Normal file
View 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/).

View File

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

19
src/webapp/jsconfig.json Normal file
View File

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

17476
src/webapp/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
src/webapp/package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "libreevent",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"core-js": "^3.8.3",
"pinia": "^2.0.34",
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vue3-draggable-resizable": "^1.6.5"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-service": "~5.0.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,25 @@
<!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">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>
<%= htmlWebpackPlugin.options.title %>
</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200">
<script defer src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

183
src/webapp/src/App.vue Normal file
View File

@@ -0,0 +1,183 @@
<!--
* 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="/login">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: #4380a8;
--overlay-color: rgba(37, 37, 37, 0.575);
--inactive-color: rgb(100, 100, 100);
--highlight-backdrop: rgb(143, 134, 192);
--PI: 3.14159265358979;
}
:root.dark {
--primary-color: white;
--accent-background: rgb(100, 100, 190);
--secondary-color: black;
--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);
}
@media ( prefers-color-scheme: dark ) {
:root {
--primary-color: white;
--popup-color: rgb(58, 58, 58);
--accent-background: rgb(100, 100, 190);
--secondary-color: black;
--background-color: rgb(32, 32, 32);
--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);
}
}
::selection {
background-color: var( --highlight-backdrop );
}
#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
}
</style>
<script>
export default {
name: 'app',
data () {
return {
theme: '',
}
},
methods: {
changeTheme () {
if ( this.theme === '&#9788;' ) {
document.documentElement.classList.remove( 'dark' );
document.documentElement.classList.add( 'light' );
sessionStorage.setItem( 'theme', '&#9789;' );
this.theme = '&#9789;';
} else if ( this.theme === '&#9789;' ) {
document.documentElement.classList.remove( 'light' );
document.documentElement.classList.add( 'dark' );
sessionStorage.setItem( 'theme', '&#9788;' );
this.theme = '&#9788;';
}
}
},
created () {
this.theme = sessionStorage.getItem( 'theme' ) ? sessionStorage.getItem( 'theme' ) : '';
if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches || this.theme === '&#9788;' ) {
document.documentElement.classList.add( 'dark' );
this.theme = '&#9788;';
} else {
document.documentElement.classList.add( 'light' );
this.theme = '&#9789;';
}
}
}
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1,35 @@
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<div>
<h1>Welcome to libreevent!</h1>
<p>Let's start the setup by entering the setup key below! You may define a setup key in the config file of libreevent. See <a href="https://libreevent.janishutz.com/docs/setup/setup" target="_blank">here</a> for more instructions</p>
<form>
<label for="key">Your setup key</label><br>
<input type="text" v-model="formData[ 'key' ]" required name="key" id="key">
</form>
<button @click="setup();" class="button">Start setup</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
formData: {},
}
},
methods: {
setup () {
this.$router.push( '/setup' );
}
},
}
</script>
<style scoped>
img {
width: 20%;
}
</style>

View File

@@ -0,0 +1,337 @@
<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 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: '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 ) {
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;
}
.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>

View 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>

View File

@@ -0,0 +1,106 @@
<!--
* 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>
<table v-if="active">
<tr>
<td>Position X:</td>
<td>
<input type="number" min="20" v-model="internal[ active ].x" @focusout="resubmit()">
</td>
</tr>
<tr>
<td>Position Y:</td>
<td>
<input type="number" min="20" v-model="internal[ active ].y" @focusout="resubmit()">
</td>
</tr>
<tr>
<td>Width:</td>
<td>
<input type="number" min="20" v-model="internal[ active ].w" @focusout="resubmit()">
</td>
</tr>
<tr>
<td>Height:</td>
<td>
<input type="number" min="20" v-model="internal[ active ].h" @focusout="resubmit()">
</td>
</tr>
<tr>
<td>Origin:</td>
<td>
<input type="number" min="1" max="4" v-model="internal[ active ].origin" @focusout="resubmit()">
</td>
</tr>
<tr>
<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 Object 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,
},
active: {
type: Number,
"default": 1,
},
},
data () {
return {
internal: {},
}
},
methods: {
loadInternal () {
for ( let value in this.draggables ) {
this.internal[ value ] = this.draggables[ value ];
}
},
resubmit () {
this.$emit( 'updated', this.internal );
}
},
watch: {
draggables ( value ) {
this.loadInternal();
},
active ( value ) {
this.loadInternal();
}
},
created () {
this.loadInternal();
}
}
</script>

View File

@@ -0,0 +1,297 @@
<!--
* libreevent - window.vue
*
* Created by Janis Hutz 05/12/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div id="window">
<properties class="properties" v-model:draggables="draggables" @updated="handleUpdate" :scale-factor="scaleFactor" :active="active"></properties>
<div class="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" :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.kind == 'seat'" :scale-factor="scaleFactor" :w="draggable.w" :h="draggable.h" :origin="draggable.origin"></circularSeatplanComponent>
<trapezoidSeatplanComponent v-if="draggable.shape == 'trapezoid' && draggable.kind == 'seat'" :scale-factor="scaleFactor" :w="draggable.w" :h="draggable.h" :origin="draggable.origin"></trapezoidSeatplanComponent>
<rectangularSeatplanComponent v-if="draggable.shape == 'rectangular' && draggable.kind == 'seat'" :scale-factor="scaleFactor" :w="draggable.w" :h="draggable.h" :origin="draggable.origin"></rectangularSeatplanComponent>
</Vue3DraggableResizable>
</div>
</div>
<div>
<button v-if="available.undo" @click="historyOp( 'undo' )">Undo</button>
<button v-else disabled>Undo</button>
<button v-if="available.redo" @click="historyOp( 'redo' )">Redo</button>
<button v-else disabled>Redo</button>
<button @click="addNewElement()">Add</button>
<button @click="deleteSelected()">Delete</button>
</div>
</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 'vue3-draggable-resizable/dist/Vue3DraggableResizable.css';
export default {
'name': 'window',
components: {
Vue3DraggableResizable,
properties,
circularSeatplanComponent,
rectangularSeatplanComponent,
trapezoidSeatplanComponent,
},
data() {
return {
active: 0,
draggables: { 1: { 'x': 100, 'y':100, 'h': 100, 'w': 250, 'active': false, 'draggable': true, 'resizable': true, 'id': 1, 'origin': 1, 'categories': { 1: 0 }, 'shape':'rectangular', 'kind': 'seat' } },
available: { 'redo': false, 'undo': false },
scaleFactor: 1,
sizePoll: null,
prevSize: { 'h': window.innerHeight, 'w': window.innerWidth },
}
},
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;
/*
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.save();
} 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();
}
};
this.loadSeatplan();
// TODO: build Zoom function (including touch gesture support)
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 = [];
// 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;
/*
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 );
} 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 );
} 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 );
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;
}
}
},
save () {
sessionStorage.setItem( 'seatplan', JSON.stringify( this.scaleDown( this.draggables ) ) );
},
addNewElement () {
this.draggables[ Object.keys( this.draggables ).length + 1 ] = { 'x': 100, 'y':100, 'h': 100, 'w': 250, 'active': false, 'draggable': true, 'resizable': true, 'id': Object.keys( this.draggables ).length + 1, 'origin': 1, 'categories': { 1: 0 }, 'shape':'rectangular', 'kind': 'seat' };
this.saveHistory();
},
deleteSelected () {
this.draggables[ this.active ].active = true;
if ( confirm( 'Do you really want to delete the selected item?' ) ) {
delete this.draggables[ this.active ];
this.saveHistory();
}
},
handleUpdate ( value ) {
this.draggables = value;
this.selectedObject = value;
this.saveHistory();
}
},
created () {
this.runHook();
this.sizePoll = setInterval( this.eventHandler, 250 );
},
unmounted() {
clearInterval( this.sizePoll );
},
}
</script>
<style scoped>
.parent {
aspect-ratio: 16 / 9;
max-height: 90vh;
max-width: 90vw;
left: 5%;
position: relative;
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: 75vh;
top: 10vh;
left: 79vw;
}
.content-parent {
aspect-ratio: 16 / 9;
height: 400%;
}
</style>

View File

@@ -0,0 +1,17 @@
<template>
<div>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>

View File

@@ -0,0 +1,105 @@
<!--
* 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,
},
},
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 = 0; row < count; row++ ) {
let nn = row * ( Math.PI / 2 );
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` };
}
}
}
},
setScaleFactor () {
for ( let row in this.seats ) {
for ( let seat in this.seats[ row ] ) {
let styles = this.seats[ row ][ seat ].style.substring( this.seats[ row ][ seat ].style.indexOf( ';' ) + 1 );
this.seats[ row ][ seat ].style = `font-size: ${this.scaleFactor * 200}%;` + styles;
}
}
}
},
watch: {
scaleFactor() {
this.setScaleFactor();
},
h() {
this.calculateChairs();
},
w() {
this.calculateChairs();
},
origin() {
this.calculateChairs();
}
},
created() {
this.calculateChairs();
}
}
</script>

View File

@@ -0,0 +1,101 @@
<!--
* 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,
},
},
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++ ) {
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;` };
}
}
}
},
setScaleFactor () {
for ( let row in this.seats ) {
for ( let seat in this.seats[ row ] ) {
let styles = this.seats[ row ][ seat ].style.substring( this.seats[ row ][ seat ].style.indexOf( ';' ) + 1 );
this.seats[ row ][ seat ].style = `font-size: ${this.scaleFactor * 200}%;` + styles;
}
}
}
},
watch: {
scaleFactor() {
this.setScaleFactor();
},
h() {
this.calculateChairs();
},
w() {
this.calculateChairs();
},
origin() {
this.calculateChairs();
}
},
created() {
this.calculateChairs();
}
}
</script>

View File

@@ -0,0 +1,108 @@
<!--
* 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,
},
},
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 = 0; row < count; row++ ) {
let nn = 2 + ( row - 1 ) * 2;
this.seats[ row ] = {};
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 * size - 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 * size - 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 * size - side ) * this.scaleFactor }px; rotate: ${ Math.PI - angle }rad` };
}
}
}
},
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>

38
src/webapp/src/main.js Normal file
View File

@@ -0,0 +1,38 @@
/*
* 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 => {
res.json().then( data => {
userStore.setUserAuth( data.user );
userStore.setAdminAuth( data.admin );
app.use( router );
app.mount( '#app' );
} );
} );
} else {
// userStore.setUserAuth( true );
app.use( router );
app.mount( '#app' );
}

View File

@@ -0,0 +1,75 @@
/*
* 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: 'admin-accounts',
name: 'adminAccounts',
component: () => import( '../views/admin/AccountView.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,
}
},
]
}

View File

@@ -0,0 +1,73 @@
/*
* 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( process.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 === 'purchase' && authRequired ) {
return { name: 'login' };
} 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' ) {
return { name: 'setupStart' };
}
} );
let doSetup = true;
if ( doSetup ) {
import( '@/router/setupRoutes' ).then( data => {
router.addRoute( data.default );
setTimeout( function () {
router.replace( window.location.pathname );
}, 200 );
} );
}
export default router;

View File

@@ -0,0 +1,118 @@
/*
* 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/OrderView.vue' ),
meta: {
title: 'Order ticket - libreevent'
}
},
{
path: '/login',
name: 'login',
component: () => import( '../views/LoginView.vue' ),
meta: {
title: 'Login - libreevent'
}
},
{
path: '/admin/login',
name: 'adminLogin',
component: () => import( '../views/AdminLoginView.vue' ),
meta: {
title: 'Login :: Admin - libreevent'
}
},
{
path: '/signup',
name: 'signup',
component: () => import( '../views/SignupView.vue' ),
meta: {
title: 'Signup - libreevent'
}
},
{
path: '/tickets/details',
name: 'ticketDetails',
component: () => import( '../views/TicketsDetailsView.vue' ),
meta: {
title: 'Details - libreevent',
transition: 'scale'
}
},
{
path: '/tickets/order',
name: 'ticketOrder',
component: () => import( '../views/TicketsOrderingView.vue' ),
meta: {
title: 'Order ticket - libreevent',
transition: 'scale'
}
},
{
path: '/cart',
name: 'cart',
component: () => import( '../views/CartView.vue' ),
meta: {
title: 'Cart - libreevent',
transition: 'scale'
}
},
{
path: '/purchase',
name: 'purchase',
component: () => import( '@/views/PurchaseView.vue' ),
meta: {
title: 'Pay - libreevent',
transition: 'scale'
}
},
{
path: '/pay',
name: 'pay',
component: () => import( '@/views/PaymentView.vue' ),
meta: {
title: 'Pay - libreevent',
transition: 'scale',
}
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import( '@/views/404.vue')
},
{
path: '/admin/seatplan',
name: 'adminSeatplanEditor',
component: () => import( '@/views/admin/events/EditorView.vue' ),
meta: {
title: 'Seatplan Editor :: Admin - libreevent',
adminAuthRequired: true,
}
},
]

View File

@@ -0,0 +1,83 @@
/*
* libreevent - setupRoutes.js
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
export default {
path: '/setup',
name: 'setup',
component: () => import( '../views/SetupView.vue' ),
meta: {
title: 'Login :: Admin - libreevent',
adminAuthRequired: true,
},
children: [
{
path: '',
name: 'setupStart',
component: () => import( '../views/setup/SetupStartView.vue' ),
meta: {
title: 'Start :: Setup - libreevent',
adminAuthRequired: true,
}
},
{
path: 'root',
name: 'setupRoot',
component: () => import( '../views/setup/SetupRootView.vue' ),
meta: {
title: 'Root account :: Setup - libreevent',
adminAuthRequired: true,
}
},
{
path: 'page',
name: 'setupPage',
component: () => import( '../views/setup/SetupPageView.vue' ),
meta: {
title: 'Landing page :: Setup - libreevent',
adminAuthRequired: true,
}
},
{
path: 'payments',
name: 'setupPayments',
component: () => import( '../views/setup/SetupPaymentsView.vue' ),
meta: {
title: 'Payments :: Setup - libreevent',
adminAuthRequired: true,
}
},
{
path: 'events',
name: 'setupEvents',
component: () => import( '../views/setup/SetupEventsView.vue' ),
meta: {
title: 'Events :: Setup - libreevent',
adminAuthRequired: true,
}
},
{
path: 'tos',
name: 'setupTOS',
component: () => import( '../views/setup/SetupTOSView.vue' ),
meta: {
title: 'TOS (Optional) :: Setup - libreevent',
adminAuthRequired: true,
}
},
{
path: 'complete',
name: 'setupComplete',
component: () => import( '../views/setup/SetupCompleteView.vue' ),
meta: {
title: 'Setup complete :: Setup - libreevent',
adminAuthRequired: true,
}
},
]
}

View 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': false } ),
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' ) ) : {};
}
}
} );

View File

@@ -0,0 +1,26 @@
/*
* 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': true, 'userPermissions': {} } ),
getters: {
getUserAuthenticated: ( state ) => state.isUserAuth,
getAdminAuthenticated: ( state ) => state.isAdminAuth,
},
actions: {
setUserAuth ( auth ) {
this.isUserAuth = auth;
},
setAdminAuth ( auth ) {
this.isAdminAuth = auth;
}
}
} );

View File

@@ -0,0 +1,44 @@
<!--
* 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">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;
}
</style>

View File

@@ -0,0 +1,75 @@
<!--
* libreevent - AdminLoginView.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 into your admin account</h1>
<form>
<label for="mail">Email address</label><br>
<input type="email" v-model="formData[ 'mail' ]" name="mail" id="mail" required><br><br>
<label for="password">Password</label><br>
<input type="text" v-model="formData[ 'password' ]" name="password" id="password" required>
</form>
<button @click="login();" class="button">Log in</button>
</div>
</div>
</template>
<script>
export default {
data () {
return {
formData: {}
}
},
methods: {
login () {
this.$router.push( '/admin' );
}
},
}
</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>
<style>
nav {
display: block;
}
</style>

View File

@@ -0,0 +1,135 @@
<!--
* 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>
</div>
</template>
<style scoped>
.cart {
text-align: justify;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.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>

View File

@@ -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
*
*
-->

View File

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

View File

@@ -0,0 +1,80 @@
<!--
* 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>
</div>
</template>
<script>
import { useUserStore } from '@/stores/userStore';
import { mapStores } from 'pinia';
export default {
data () {
return {
formData: {}
}
},
computed: {
...mapStores( useUserStore )
},
methods: {
login () {
this.userStore.setUserAuth( true );
this.$router.push( sessionStorage.getItem( 'redirect' ) ? sessionStorage.getItem( 'redirect' ) : '/account' );
sessionStorage.removeItem( 'redirect' );
}
},
}
</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%;
}
</style>

View File

@@ -0,0 +1,102 @@
<!--
* 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 v-for="event in events">
<li>
<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.date }}</p>
<h4>Starting at {{ event.currency }} {{ event.startingPrice }}</h4>
</div>
<img :src="require( '@/assets/' + 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':'TestDate', 'startingPrice':15, 'location': 'TestLocation', 'eventID': 'test', 'currency': 'CHF', 'logo': 'logo.png' }, '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':'TestDate', 'startingPrice':15, 'location': 'TestLocation', 'eventID': 'test2', 'currency': 'CHF', 'logo': 'logo.png' } }
}
}
};
</script>

View File

@@ -0,0 +1,8 @@
<!--
* libreevent - PaymentPrepareView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->

View File

@@ -0,0 +1,64 @@
<!--
* 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>

View File

@@ -0,0 +1,193 @@
<!--
* 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>
<router-link to="/pay">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>
.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>

View File

@@ -0,0 +1,73 @@
<!--
* libreevent - SetupView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<nav class="setup-nav">
<router-link to="/setup">Start</router-link> |
<router-link to="/setup/root" v-if="backendStore.getVisitedSetupPages[ 'root' ]">Root account</router-link>
<a v-else class="inactive">Root account</a> |
<router-link to="/setup/page" v-if="backendStore.getVisitedSetupPages[ 'page' ]">Landing page</router-link>
<a v-else class="inactive">Landing page</a> |
<router-link to="/setup/payments" v-if="backendStore.getVisitedSetupPages[ 'payments' ]">Payments</router-link>
<a v-else class="inactive">Payments</a> |
<router-link to="/setup/events" v-if="backendStore.getVisitedSetupPages[ 'events' ]">Events</router-link>
<a v-else class="inactive">Events</a> |
<router-link to="/setup/tos" v-if="backendStore.getVisitedSetupPages[ 'tos' ]">TOS</router-link>
<a v-else class="inactive">TOS</a> |
<router-link to="/setup/complete" v-if="backendStore.getVisitedSetupPages[ 'complete' ]">Complete</router-link>
<a v-else class="inactive">Complete</a>
</nav>
<h1>Setup</h1>
<div class="main-view">
<router-view v-slot="{ Component, route }">
<transition :name="route.meta.transition || 'scale'" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</div>
</template>
<script>
import { useBackendStore } from '@/stores/backendStore';
import { mapStores } from 'pinia';
export default {
data () {
return {
formData: {}
}
},
computed: {
...mapStores( useBackendStore )
},
created () {
this.backendStore.loadVisitedSetupPages();
},
};
</script>
<style scoped>
.inactive {
color: var( --inactive-color );
cursor: not-allowed;
}
.setup-nav {
display: block;
}
</style>
<style>
nav {
display: none;
}
</style>

View 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>

View File

@@ -0,0 +1,42 @@
<!--
* 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">
<h1>Details</h1>
<router-link to="/tickets"><span class="material-symbols-outlined" style="font-size: 100%;">arrow_back</span>Back</router-link>
<h3>{{ event.name }}</h3>
<p>{{ event.description }}</p>
<router-link to="/tickets/order">Order tickets</router-link>
</div>
</template>
<style scoped>
.details {
flex-grow: 1;
}
</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>

View 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.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>

View File

@@ -0,0 +1,31 @@
<!--
* libreevent - AccountView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h2>Admin Accounts</h2>
<p>Here you can change everything regarding admin accounts. You may create, modify or delete admin accounts.</p>
<div class="bigButtons"></div>
</div>
</template>
<script>
export default {
data () {
return {
formData: {}
}
},
methods: {
setup () {
}
}
};
</script>

View File

@@ -0,0 +1,133 @@
<!--
* 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>
<nav class="side-nav">
<router-link to="/admin" class="admin-menu">Home</router-link>
<router-link to="/admin/admin-accounts" class="admin-menu">Admin Accounts</router-link>
<router-link to="/admin/pages" class="admin-menu">Pages</router-link>
<router-link to="/admin/events" class="admin-menu">Events</router-link>
<router-link to="/admin/plugins" class="admin-menu">Plugins</router-link>
<router-link to="/admin/settings" class="admin-menu">Settings</router-link>
<button to="/admin/login" class="admin-menu" @click="logout()">Logout</button>
</nav>
<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: 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%;
min-height: 80vh;
overflow: scroll;
}
.side-nav {
grid-area: sidebar;
display: flex;
flex-direction: column;
height: 100%;
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' );
} );
}
}
}
};
</script>

View File

@@ -0,0 +1,39 @@
<!--
* libreevent - EventsView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<div class="main-view">
<h2>Events</h2>
<ul>
<li v-for="event in events"></li>
</ul>
<router-view v-slot="{ Component, route }">
<transition :name="route.meta.transition || 'fade'" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</div>
</template>
<script>
export default {
data () {
return {
events: {},
}
},
methods: {
setup () {
}
}
};
</script>

View 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/admin-accounts" class="bigButton">Admin-accounts</router-link>
<router-link to="/admin/pages" class="bigButton">Pages</router-link>
<router-link to="/admin/events" class="bigButton">Events</router-link>
<router-link to="/admin/events" class="bigButton">Plugins</router-link>
<router-link to="/admin/events" class="bigButton">Settings</router-link>
<a href="https://libreevent.janishutz.com/docs/admin-panel" class="bigButton" target="_blank">Documentation</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>

View File

@@ -0,0 +1,31 @@
<!--
* 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>Welcome to the admin panel!</p>
<div class="bigButtons"></div>
</div>
</template>
<script>
export default {
data () {
return {
formData: {}
}
},
methods: {
setup () {
}
}
};
</script>

View 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>

View File

@@ -0,0 +1,179 @@
<!--
* 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>
<!-- TODO: Move to per event settings -->
<table class="settings-toggles">
<tr class="settings-option" v-for="setting in settings">
<td class="info-wrapper" @mouseenter="showInfo( setting.id )" @mouseleave="hideInfo( setting.id )">
{{ setting.display }}
<span class="material-symbols-outlined info-icon">info</span>
<div class="info-box" :id="setting.id">
<div class="info-box-container">
{{ setting.tooltip }}
</div>
</div>
</td>
<td>
<label class="switch">
<input type="checkbox" v-model="setting.value">
<span class="slider round"></span>
</label>
</td>
</tr>
</table>
</div>
</template>
<script>
export default {
data () {
return {
settings: {
'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'
}
}
}
},
methods: {
showInfo ( box ) {
$( '#' + box ).stop();
$( '#' + box ).fadeIn( 300 );
},
hideInfo ( box ) {
$( '#' + box ).stop();
$( '#' + box ).fadeOut( 300 );
}
}
};
</script>
<style scoped>
.settings-toggles {
width: 80%;
}
.info-wrapper {
display: inline;
position: relative;
}
.info-icon {
font-size: 100%;
cursor: default;
}
.info-box {
display: none;
position: absolute;
z-index: 10;
width: 20vw;
height: 20vh;
background-color: var( --popup-color );
border-radius: 20px;
top: 125%;
left: -50%
}
.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( --popup-color ) transparent;
}
.info-box-container {
display: flex;
width: 80%;
height: 80%;
padding: 10%;
padding-top: 5%;
align-items: center;
justify-content: center;
}
/* https://www.w3schools.com/howto/tryit.asp?filename=tryhow_css_switch */
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
</style>

View File

@@ -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>Seatplan Editor</h2>
<window />
</div>
</template>
<script>
import window from '@/components/seatplan/editor/window.vue';
export default {
data () {
return {
formData: {}
}
},
components: {
window,
},
methods: {
setup () {
}
}
};
</script>
<style>
nav {
display: none;
}
</style>

View File

@@ -0,0 +1,15 @@
<!--
* libreevent - SetupCompleteView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h2>Setup was completed!</h2>
<router-link to="/admin">To the admin panel</router-link>
</div>
</template>

View File

@@ -0,0 +1,39 @@
<!--
* libreevent - SetupEventsView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h3>Setting up Events</h3>
<p>You may choose all of the below payment methods, but we recommend to only select one payment gateway for simplicity. Recommended: Either Stripe or Adyen. See the comparison of the different payment methods <a href="https://libreevent.janishutz.com/docs/payments">here</a></p>
<p>You may find more infos about this part <a href="https://libreevent.janishutz.com/docs/setup/setup#payment-methods" target="_blank">here</a></p>
<button @click="submit()">Continue</button>
</div>
</template>
<script>
import { useBackendStore } from '@/stores/backendStore';
import { mapStores } from 'pinia';
export default {
data () {
return {
formData: {}
}
},
computed: {
...mapStores( useBackendStore )
},
methods: {
submit () {
this.backendStore.addVisitedSetupPages( 'tos', true );
this.$router.push( '/setup/tos' );
}
},
};
</script>

View File

@@ -0,0 +1,45 @@
<!--
* libreevent - SetupPageView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h3>Setting up the landing page</h3>
<p>The landing page is the page your customers see when they visit your webpage. You may select a page template <a href="https://libreevent.janishutz.com/docs/homepage/templates">here</a>.</p>
<p>You may find more infos about this part <a href="https://libreevent.janishutz.com/docs/setup/setup#page-setup" target="_blank">here</a></p>
<label for="template">Choose a template</label><br>
<select name="template" id="template" v-for="option in options">
<option :value="option.id">{{ option.name }}</option>
</select><br>
<button @click="submit()">Continue</button>
</div>
</template>
<script>
import { useBackendStore } from '@/stores/backendStore';
import { mapStores } from 'pinia';
export default {
data () {
return {
options: { 'default': { 'id': 'default', 'name': 'Default' }},
}
},
computed: {
...mapStores( useBackendStore )
},
methods: {
submit () {
this.backendStore.addVisitedSetupPages( 'payments', true );
this.$router.push( '/setup/payments' );
}
},
};
</script>

View File

@@ -0,0 +1,39 @@
<!--
* libreevent - SetupPaymentsView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h3>Setting up payment methods</h3>
<p>You may choose all of the below payment methods, but we recommend to only select one payment gateway for simplicity. Recommended: Either Stripe or Adyen. See the comparison of the different payment methods <a href="https://libreevent.janishutz.com/docs/payments">here</a></p>
<p>You may find more infos about this part <a href="https://libreevent.janishutz.com/docs/setup/setup#payment-methods" target="_blank">here</a></p>
<button @click="submit()">Continue</button>
</div>
</template>
<script>
import { useBackendStore } from '@/stores/backendStore';
import { mapStores } from 'pinia';
export default {
data () {
return {
formData: {}
}
},
computed: {
...mapStores( useBackendStore )
},
methods: {
submit () {
this.backendStore.addVisitedSetupPages( 'events', true );
this.$router.push( '/setup/events' );
}
},
};
</script>

View File

@@ -0,0 +1,54 @@
<!--
* libreevent - SetupRootView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h3>Setting up the root account</h3>
<p>The root account is the most powerful account. Therefore, it should only be used if really necessary and should have a strong password. It also always requires Two Factor Authentication for added security. You may log into the root account by typing 'root' into the Email/Username field on the admin login screen.</p>
<p>You may find more infos about this part <a href="https://libreevent.janishutz.com/docs/setup/setup#root-account" target="_blank">here</a></p>
<p>Password requirements:</p>
<ul style="list-style: none;">
<li>At least 15 characters long</li>
<li>At least 2 special characters</li>
<li>At least 2 numbers</li>
<li>At least 2 lower and 2 upper case letters</li>
</ul>
<form>
<label for="mail">Email address for 2FA</label><br>
<input type="email" name="mail" id="mail"><br>
<label for="password">Password</label><br>
<input type="email" name="password" id="password"><br>
<label for="password2">Confirm password</label><br>
<input type="email" name="password2" id="password2">
</form>
<button @click="submit()">Continue</button>
</div>
</template>
<script>
import { useBackendStore } from '@/stores/backendStore';
import { mapStores } from 'pinia';
export default {
data () {
return {
formData: {}
}
},
computed: {
...mapStores( useBackendStore )
},
methods: {
submit () {
this.backendStore.addVisitedSetupPages( 'page', true );
this.$router.push( 'page' );
}
},
};
</script>

View File

@@ -0,0 +1,38 @@
<!--
* libreevent - SetupStartView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h2>Welcome to libreevent!</h2>
<i style="font-size: small;">All links during setup open in separate tabs</i>
<p>Let's start by setting it up. We strongly encourage you to also have a look at the extensive documentation of the setup process <a href="https://libreevent.janishutz.com/docs/setup/setup" target="_blank">here</a></p>
<router-link to="/setup/root" @click="setup();">Start setup</router-link>
</div>
</template>
<script>
import { useBackendStore } from '@/stores/backendStore';
import { mapStores } from 'pinia';
export default {
data () {
return {
formData: {}
}
},
computed: {
...mapStores( useBackendStore )
},
methods: {
setup () {
this.backendStore.addVisitedSetupPages( 'root', true );
}
},
};
</script>

View File

@@ -0,0 +1,39 @@
<!--
* libreevent - SetupTOSView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div>
<h3>Setting up TOS (optional)</h3>
<p>You may choose all of the below payment methods, but we recommend to only select one payment gateway for simplicity. Recommended: Either Stripe or Adyen. See the comparison of the different payment methods <a href="https://libreevent.janishutz.com/docs/payments" target="_blank">here</a></p>
<p>You may find more infos about this part <a href="https://libreevent.janishutz.com/docs/setup/setup#payment-methods" target="_blank">here</a></p>
<button @click="submit()">Continue</button>
</div>
</template>
<script>
import { useBackendStore } from '@/stores/backendStore';
import { mapStores } from 'pinia';
export default {
data () {
return {
formData: {}
}
},
computed: {
...mapStores( useBackendStore )
},
methods: {
submit () {
this.backendStore.addVisitedSetupPages( 'complete', true );
this.$router.push( '/setup/complete' );
}
},
};
</script>

4
src/webapp/vue.config.js Normal file
View File

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

14
website/README.md Normal file
View File

@@ -0,0 +1,14 @@
# Website
The main website's pages are written in HTML & CSS whilst the doc pages are automatically generated by the script in [./build](build/).
** DO NOT MODIFY THE DOC PAGES IN THE [dist/docs/](dist/docs/) DIRECTORY! **
## Links in the md files in [src/](src/)
Please note that you are required to use either a link relative to the root folder with double forward slash (example: *//server/app.js*), to the website root with a single forward slash (example: */download*), to the docs root with &/ (example: *&/setup*) or simply /docs/ (example: */docs/setup*), a full link (example: *https://libreevent.janishutz.com/docs*) or a relative link (example: *plugins/music*). If you do not follow these patterns, the website won't build or the links will not work correctly.
***NOTE: Don't be confused if the root folder links don't work in the Markdown, as they use specific syntax for the build script.***
## Folder structure, file naming and file structure
Please note that the filename that is used for the md file is used to create a folder that contains an *index.html* so the *.html* file extension disappears. The build script uses the first level 1 title (marked with \# in md) as the page title and also for the navigation menu, so please choose the title appropriately and don't let it exceed 50 characters!
## Modifying the files in [dist/](dist/)
You may modify all the files in the [dist/](dist/) directory except the files in [dist/docs/](dist/docs/).

25
website/build.js Normal file
View File

@@ -0,0 +1,25 @@
/*
* libreevent - build.js
*
* Created by Janis Hutz 03/09/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
const prompt = require( 'prompt-sync' );
const markdownIt = require( 'markdown-it' );
const md2html = new markdownIt();
if ( prompt( 'Do you want to rebuild the ' ).toLowercase === 'y' ) {
buildDocs();
buildNav();
}
function buildNav () {
}
function buildDocs () {
md2html.render( '#Test' );
}

50
website/dist/about/index.html vendored Normal file
View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<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">
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/home.css">
<title>About :: libreǝvent</title>
<meta name="description" content="Looking for a free and open source event management solution you can host yourself? libreevent is a project that does exactly that.">
</head>
<body>
<div id="nav"></div>
<div id="backToTop" onclick="backToTop();"></div>
<div class="title-wrapper">
<h1>About <i>libre</i>ǝvent</h1>
<footer>Learn more about the project!</footer>
</div>
<div class="content">
<h3 class="dev">Website & project not completed yet!</h3>
<h3>libreǝvent is a free and open source event management solution that you can host on your own hardware, if you want to</h3>
<p>It is mainly written in node.js, but, as a webapp, uses also lots of HTML and CSS. For storing the customer / userdata, you may choose between using MySQL and a JSON based custom database, such that this project may be used in more cases. Whilst it is recommended to use MySQL with this project, using the JSON based database can be an easy to set up alternative if you are not serving that many customers.</p>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.3/jquery-ui.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
let btn = document.getElementById( 'backToTop' );
$( document ).ready( function () {
$( '#nav' ).load( '/nav.html' );
$( '#footer' ).load( '/footer.html' );
} );
function backToTop () {
if ( document.body.scrollTop > 500 || document.documentElement.scrollTop > 500 ) {
window.scrollTo( { top: 0, behavior: 'smooth' } );
}
}
window.onscroll = function () {
if ( document.body.scrollTop > 500 || document.documentElement.scrollTop > 500 ) {
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
} else {
btn.style.opacity = '0';
btn.style.cursor = 'default';
}
};
</script>
</body>
</html>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="102.3716mm" height="144.49777mm" viewBox="0 0 362.73401 511.99998" id="svg3476" version="1.1" inkscape:version="0.91 r13725" sodipodi:docname="CSS3.svg">
<defs id="defs3478"/>
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.35" inkscape:cx="181.367" inkscape:cy="256" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" inkscape:window-width="1366" inkscape:window-height="704" inkscape:window-x="0" inkscape:window-y="27" inkscape:window-maximized="1"/>
<metadata id="metadata3481">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g inkscape:label="Calque 1" inkscape:groupmode="layer" id="layer1" transform="translate(-193.633,-276.3622)">
<g id="g3013" transform="translate(119,276.3622)">
<polygon id="polygon2989" points="437.367,100.62 404.321,470.819 255.778,512 107.644,470.877 74.633,100.62 " style="fill:#264de4"/>
<polygon id="polygon2991" points="376.03,447.246 404.27,130.894 256,130.894 256,480.523 " style="fill:#2965f1"/>
<polygon id="polygon2993" points="150.31,268.217 154.38,313.627 256,313.627 256,268.217 " style="fill:#ebebeb"/>
<polygon id="polygon2995" points="256,176.305 255.843,176.305 142.132,176.305 146.26,221.716 256,221.716 " style="fill:#ebebeb"/>
<polygon id="polygon2997" points="256,433.399 256,386.153 255.801,386.206 205.227,372.55 201.994,336.333 177.419,336.333 156.409,336.333 162.771,407.634 255.791,433.457 " style="fill:#ebebeb"/>
<path id="path2999" d="m 160,0 55,0 0,23 -32,0 0,23 32,0 0,23 -55,0 z" inkscape:connector-curvature="0"/>
<path id="path3001" d="m 226,0 55,0 0,20 -32,0 0,4 32,0 0,46 -55,0 0,-21 32,0 0,-4 -32,0 z" inkscape:connector-curvature="0"/>
<path id="path3003" d="m 292,0 55,0 0,20 -32,0 0,4 32,0 0,46 -55,0 0,-21 32,0 0,-4 -32,0 z" inkscape:connector-curvature="0"/>
<polygon id="polygon3005" points="311.761,313.627 306.49,372.521 255.843,386.191 255.843,433.435 348.937,407.634 349.62,399.962 360.291,280.411 361.399,268.217 369.597,176.305 255.843,176.305 255.843,221.716 319.831,221.716 315.699,268.217 255.843,268.217 255.843,313.627 " style="fill:#ffffff"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

14
website/dist/assets/HTML5_Logo.svg vendored Normal file
View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<title>HTML5 Logo</title>
<polygon fill="#E44D26" points="107.644,470.877 74.633,100.62 437.367,100.62 404.321,470.819 255.778,512 "/>
<polygon fill="#F16529" points="256,480.523 376.03,447.246 404.27,130.894 256,130.894 "/>
<polygon fill="#EBEBEB" points="256,268.217 195.91,268.217 191.76,221.716 256,221.716 256,176.305 255.843,176.305 142.132,176.305 143.219,188.488 154.38,313.627 256,313.627"/>
<polygon fill="#EBEBEB" points="256,386.153 255.801,386.206 205.227,372.55 201.994,336.333 177.419,336.333 156.409,336.333 162.771,407.634 255.791,433.457 256,433.399"/>
<path d="M108.382,0h23.077v22.8h21.11V0h23.078v69.044H152.57v-23.12h-21.11v23.12h-23.077V0z"/>
<path d="M205.994,22.896h-20.316V0h63.72v22.896h-20.325v46.148h-23.078V22.896z"/>
<path d="M259.511,0h24.063l14.802,24.26L313.163,0h24.072v69.044h-22.982V34.822l-15.877,24.549h-0.397l-15.888-24.549v34.222h-22.58V0z"/>
<path d="M348.72,0h23.084v46.222h32.453v22.822H348.72V0z"/>
<polygon fill="#FFFFFF" points="255.843,268.217 255.843,313.627 311.761,313.627 306.49,372.521 255.843,386.191 255.843,433.435 348.937,407.634 349.62,399.962 360.291,280.411 361.399,268.217 349.162,268.217"/>
<polygon fill="#FFFFFF" points="255.843,176.305 255.843,204.509 255.843,221.605 255.843,221.716 365.385,221.716 365.385,221.716 365.531,221.716 366.442,211.509 368.511,188.488 369.597,176.305"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
website/dist/assets/JavaScript-logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

1
website/dist/assets/Node.js_logo.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
website/dist/assets/htmlCSSJS.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
website/dist/assets/htmlCSSJS.xcf vendored Normal file

Binary file not shown.

BIN
website/dist/assets/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

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