Browse Source

gtblank

master
Andreas Demmelbauer 1 year ago
parent
commit
c7a6d39e5e
13 changed files with 1194 additions and 278 deletions
  1. BIN
     
  2. BIN
     
  3. BIN
     
  4. +49
    -6
      src/router/index.js
  5. +5
    -1
      src/views/Cancel.vue
  6. +0
    -271
      src/views/Home.vue
  7. +113
    -0
      src/views/Invalidate.vue
  8. +67
    -0
      src/views/Reservation.vue
  9. +287
    -0
      src/views/Reservations.vue
  10. +36
    -0
      src/views/events/gtblank-init/GtBlankDirections.vue
  11. +315
    -0
      src/views/events/gtblank-init/GtblankInit.vue
  12. BIN
     
  13. +322
    -0
      src/views/events/okt-28/Okt28.vue

BIN
View File


BIN
View File


BIN
View File


+ 49
- 6
src/router/index.js View File

@@ -1,6 +1,5 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

@@ -8,15 +7,49 @@ const routes = [
{
path: '/',
name: 'Home',
component: Home
component: () => import('../views/Home.vue')
},
// {
// path: '/event/gtblank-init',
// name: 'GtblankInit',
// meta: { title: 'gtblank - init (content warning: flashy lights)' },
// component: () => import('../views/events/gtblank-init/GtblankInit.vue')
// },
{
path: '/event/gtblank-init/directions',
name: 'GtblankInit',
meta: { title: 'gtblank - init >> directions' },
component: () => import('../views/events/gtblank-init/GtBlankDirections.vue')
},
// {
// path: '/event/okt28',
// name: 'Okt28',
// component: () => import('../views/events/okt-28/Okt28.vue')
// },
{
path: '/event/ignaz',
name: 'Ignaz',
component: () => import('../views/events/ignaz/Ignaz.vue')
},
{
path: '/cancel/:token',
name: 'Cancel',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/Cancel.vue')
component: () => import('../views/Cancel.vue')
},
{
path: '/reservation/:token',
name: 'Reservation',
component: () => import('../views/Reservation.vue')
},
{
path: '/invalidate',
name: 'Reservation',
component: () => import('../views/Invalidate.vue')
},
{
path: '/reservations/:secret',
name: 'Reservations',
component: () => import('../views/Reservations.vue')
}
]

@@ -26,4 +59,14 @@ const router = new VueRouter({
routes
})

const DEFAULT_TITLE = 'event reservation';
router.afterEach((to) => {
// Use next tick to handle router history correctly
// see: https://github.com/vuejs/vue-router/issues/914#issuecomment-384477609
Vue.nextTick(() => {
document.title = to.meta.title || DEFAULT_TITLE;
});
});


export default router

+ 5
- 1
src/views/Cancel.vue View File

@@ -1,5 +1,5 @@
<template>
<div class="cancel">
<div class="cancel-view">
<h1 v-if="!success">cancelling reservation {{$route.params.token}} ...</h1>
<h1 v-else>
your reservation got cancelled.<br>
@@ -60,4 +60,8 @@
</script>

<style>
.cancel-view {
min-height: 100vh;
margin: 2em;
}
</style>

+ 0
- 271
src/views/Home.vue View File

@@ -1,296 +1,25 @@
<template>
<div id="app">
<div class="selection-text">{{selectionText}}</div>
<div class="otf" id="teilnehmen">
<!-- <h1 style="padding-top:1em">Party</h1> -->
<div class="form-wrapper">
</div>
<form v-if="!success" class="input-form" @submit.prevent="onClickTheButton" style="margin-top: 5vh">
<div class="input-headline">reservation</div>
<input class="input-text" v-model="anzeigename" placeholder="nickname (public)" size="1" />
<input class="input-text" v-model="name" placeholder="real name" size="1" />
<input class="input-text" v-model="email" placeholder="email" size="1" />
<!-- <label class="input-cb" for="newsletter">
<input name="newsletter" id="newsletter" type="checkbox" class="input-checkbox" />stay informed
</label> -->
<input class="input-btn" type="submit" :value="(sending ? 'send ...' : 'reserve')" />
<div v-if="error">
oh no something went wrong!<br>
{{errorReason.detail}}
</div>
</form>
<div v-if="showHelp" style="margin-bottom: 1em;">
*robot voice* give us your data for the reservation!
</div>
<div v-if="success">
<p style="font-size: 8vw">
Thanks!
</p>
<button @click="resetForm" class="input-btn">✓ see you there!</button>
</div>
<vue-word-cloud class="wordcloud" style="height: 70vh; width: 80%" :words="words"
:color="() => { return nameColors[Math.floor(Math.random() * nameColors.length)] }"
font-family="'JetBrains Mono'" />
</div>
<!-- <div class="details" ref="details">
TEXT!
</div> -->
<div
v-if="canShare"
@click="share"
style="cursor: pointer; margin-top: 5em;"
>
<svg class="share" fill="#fff" version="1.1" viewBox="0 0 350.22 180.68" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-622.81 -392.3)"><path d="m659.49 392.3v16.427h-16.299v19.182h-20.385v50.605h21.942v18.918h17.175v19.63h55.254l-37.054 55.916h30.57l37.054-55.916h190.84v-16.06h15.991v-18.048h18.443v-50.605h-19.999v-20.05h-16.867v-19.998zm6.9343 23.419h263.37v19.998h19.999v41.866h-18.443v16.06h-263.37v-19.63h-21.942v-41.866h20.385z" stroke-width="0" style="paint-order:stroke fill markers"/><g transform="matrix(.42673 0 0 .42673 457.43 276.68)" aria-label="WILLTEILEN"><path d="m590.71 451.07h-21.891l-6.5625-29.859q-0.46875-1.875-1.5-7.4531-0.98437-5.5781-1.4531-9.3281-0.375 3.0469-1.2188 7.5938-0.84375 4.5-1.6875 8.2969-0.79688 3.7969-6.7969 30.75h-21.891l-16.969-68.531h17.859l7.4531 34.359q2.5312 11.391 3.4688 18.141 0.60937-4.7812 2.1562-12.984 1.5938-8.2031 2.9531-13.594l6.0469-25.922h17.156l5.8594 25.922q1.5 6.2344 3.0469 14.391 1.5469 8.1562 2.0625 12.188 0.60937-5.2031 3.3281-18.047l7.5938-34.453h17.859z"/><path d="m616.44 451.07v-68.531h18.609v68.531z"/><path d="m649.86 451.07v-68.531h18.516v53.578h26.391v14.953z"/><path d="m705.74 451.07v-68.531h18.516v53.578h26.391v14.953z"/><path d="m791.85 451.07h-18.516v-53.391h-16.734v-15.141h51.938v15.141h-16.688z"/><path d="m859.02 451.07h-40.688v-68.531h40.688v14.859h-22.172v10.781h20.531v14.859h-20.531v12.938h22.172z"/><path d="m871.02 451.07v-68.531h18.609v68.531z"/><path d="m904.44 451.07v-68.531h18.516v53.578h26.391v14.953z"/><path d="m1001 451.07h-40.687v-68.531h40.687v14.859h-22.172v10.781h20.531v14.859h-20.531v12.938h22.172z"/><path d="m1078.3 451.07h-24.281l-25.031-48.281h-0.4218q0.8906 11.391 0.8906 17.391v30.891h-16.406v-68.531h24.188l24.938 47.625h0.2813q-0.6563-10.359-0.6563-16.641v-30.984h16.5z"/></g></g></svg>
</div>
<a
v-else
href="mailto:?subject=invitation%20-%20oct%2028&body=%40SWK%0D%0Areservation%20required%3A%20reservation.ck.si "
style="cursor: pointer; margin-top: 5em;"
>
<svg class="share" fill="#fff" version="1.1" viewBox="0 0 350.22 180.68" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-622.81 -392.3)"><path d="m659.49 392.3v16.427h-16.299v19.182h-20.385v50.605h21.942v18.918h17.175v19.63h55.254l-37.054 55.916h30.57l37.054-55.916h190.84v-16.06h15.991v-18.048h18.443v-50.605h-19.999v-20.05h-16.867v-19.998zm6.9343 23.419h263.37v19.998h19.999v41.866h-18.443v16.06h-263.37v-19.63h-21.942v-41.866h20.385z" stroke-width="0" style="paint-order:stroke fill markers"/><g transform="matrix(.42673 0 0 .42673 457.43 276.68)" aria-label="WILLTEILEN"><path d="m590.71 451.07h-21.891l-6.5625-29.859q-0.46875-1.875-1.5-7.4531-0.98437-5.5781-1.4531-9.3281-0.375 3.0469-1.2188 7.5938-0.84375 4.5-1.6875 8.2969-0.79688 3.7969-6.7969 30.75h-21.891l-16.969-68.531h17.859l7.4531 34.359q2.5312 11.391 3.4688 18.141 0.60937-4.7812 2.1562-12.984 1.5938-8.2031 2.9531-13.594l6.0469-25.922h17.156l5.8594 25.922q1.5 6.2344 3.0469 14.391 1.5469 8.1562 2.0625 12.188 0.60937-5.2031 3.3281-18.047l7.5938-34.453h17.859z"/><path d="m616.44 451.07v-68.531h18.609v68.531z"/><path d="m649.86 451.07v-68.531h18.516v53.578h26.391v14.953z"/><path d="m705.74 451.07v-68.531h18.516v53.578h26.391v14.953z"/><path d="m791.85 451.07h-18.516v-53.391h-16.734v-15.141h51.938v15.141h-16.688z"/><path d="m859.02 451.07h-40.688v-68.531h40.688v14.859h-22.172v10.781h20.531v14.859h-20.531v12.938h22.172z"/><path d="m871.02 451.07v-68.531h18.609v68.531z"/><path d="m904.44 451.07v-68.531h18.516v53.578h26.391v14.953z"/><path d="m1001 451.07h-40.687v-68.531h40.687v14.859h-22.172v10.781h20.531v14.859h-20.531v12.938h22.172z"/><path d="m1078.3 451.07h-24.281l-25.031-48.281h-0.4218q0.8906 11.391 0.8906 17.391v30.891h-16.406v-68.531h24.188l24.938 47.625h0.2813q-0.6563-10.359-0.6563-16.641v-30.984h16.5z"/></g></g></svg>
</a>
</div>
</template>

<script>
import VueWordCloud from 'vuewordcloud';
export default {
name: 'App',
data() {
return {
anzeigename: '',
name: '',
email: '',
newsletter: false,
guests: [],
sending: false,
showHelp: false,
selection: null,
selectionText: null,
success: false,
error: false,
errorReason: null,
}
},
components: {
[VueWordCloud.name]: VueWordCloud,
},
computed: {
nameColors() {
return ['DeepPink', 'Crimson', 'Blue', 'BlueViolet', 'Fuchsia', 'Navy', 'Indigo']
},
words() {
const names = this.guests.map(guest => {
return guest.anzeigename
})
const nameLengths = names.map(name => {
return name.length
})
const maxNameLength = Math.max(...nameLengths)
return this.guests.map(guest => {
const nameLength = guest.anzeigename.length
let size = maxNameLength - nameLength + 12
return [` ${guest.anzeigename} `, size]
})
},
// guestlist() {
// return this.guests.map( (g) => {
// g.name = g.anzeigename
// return g
// })
// }
canShare () {
return !!navigator.share
}
},
methods: {
async fetchGuests() {
const response = await fetch(`${process.env.VUE_APP_API_URL}guest`)
const data = await response.json()
this.guests = data
},
async onClickTheButton() {
if (this.name.length == 0) {
this.showHelp = true
return false
}
this.showHelp = false
this.sending = true
let data = {
anzeigename: this.anzeigename,
name: this.name,
email: this.email,
newsletter: false,
}
const response = await fetch(
`${process.env.VUE_APP_API_URL}guest`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}
)
if (response.status === 200) {
this.anzeigename = ''
this.name = ''
this.email = ''
this.success = true
this.fetchGuests()
} else {
this.success = false
this.fetchGuests()
this.error = true
this.errorReason = await response.json()
}
this.sending = false
return false
},
resetForm () {
this.anzeigename = ''
this.name = ''
this.email = ''
this.success = false
},
async share () {
location.href = "#";
try {
await navigator.share({ url:"https://reservation.ck.si/" })
} catch (err) {
console.log(`Error: ${err}`)
}
}
},
mounted() {
this.fetchGuests()
document.addEventListener('selectionchange', () => {
this.selection = document.getSelection()
// console.log(this.selection)
if (this.selection) {
this.selectionText = this.selection.toString()
}
});
// this.$refs.details.$el
},
}
</script>

<style>
.otf {
width: 100%;
/* background-color: rgb(167, 204, 247); */
/* background-image: url('bg.gif'); */
background-size: cover;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
position: relative;
}


.input-headline {
color: rgb(255, 255, 255);
font-size: 2.8vh;
margin-bottom: .8vh;
}

.input-form {
display: flex;
flex-direction: column;
width: 100%;
max-width: 800px;
mix-blend-mode: exclusion;
margin-top: 4vh;
z-index: 9;
position: relative;
}

.input-text,
.input-btn,
.input-cb {
color: #000;
background: #fff;
font-size: 2.8vh;
font-family: 'JetBrains Mono', monospace;
border: 0;
padding: .5em 1em;
line-height: 1.4;
max-width: 100%;
}
.input-btn {
color: #fff;
background-color: #1e3a8e;
border-top: 2px solid rgb(0, 0, 0);
cursor: pointer;
}

.input-btn:hover {
color: #fff;
background: #3260eb;
}

.input-checkbox {
margin: 0 1em 0 0;
}


@media screen and (max-width: 1000px) {
.form-wrapper {
margin-top: 4em;
}

.input-text,
.input-btn {
width: 100%;
max-width: 100%;
min-width: 0;
text-align: center;
}
/* .input-form {
position: absolute;
bottom: 0;
display: flex;
flex-direction: column;
} */
}


.input-text {
width: auto;
flex-grow: 1;
}



.details {
color: #fff;
background-color: #000;
font-size: 10vw;
max-width: 80em;
margin: 3% 3% 200px;
padding: 3%;
}

.share {
max-width: 30em;
width: 80%;
mix-blend-mode: soft-light;
}
.share:hover {
mix-blend-mode: difference;
}

.selection-text {
position: fixed;
z-index: 9999;
font-size: 10vh;
line-height: 1em;
pointer-events: none;
color: #ec4b73;
mix-blend-mode: multiply;
top: 10%;
}
</style>

+ 113
- 0
src/views/Invalidate.vue View File

@@ -0,0 +1,113 @@
<template>
<div class="invalidation-view">
<qrcode-stream
v-if="!token"
@decode="onDecode"
@init="onInit">
</qrcode-stream>
<div v-else>
<div v-if="status && !status.valid">
reservation {{ token }} from {{status.anzeigename}} is not valid!<br>
reason: {{status.reason || status.detail}}<br><br>
<button @click="onReset" class="btn reset-btn">reset</button>
</div>
<div v-if="status && status.valid">
reservation from {{status.anzeigename}} is valid :) <br>
<div>
<button @click="onInvalidate" class="btn invalidate-btn">invalidate now</button>
<button @click="onReset" class="btn reset-btn">reset</button>
</div>
<div v-if="error">
{{error}}
</div>
</div>
</div>
<!-- {{status}} -->
</div>
</template>

<script>
import { QrcodeStream } from 'vue-qrcode-reader'
export default {
name: 'Invalidate',
components: {
QrcodeStream,
},
data() {
return {
token: null,
status: null,
success: null,
error: null,
}
},
methods: {
async onDecode(token) {
this.token = token
this.status = await this.getStatus(token)
},
async getStatus (token) {
const response = await fetch(`${process.env.VUE_APP_API_URL}guest/${token}`)
const data = await response.json()
return data
},
onInit(promise) {
promise
.then(console.log)
.catch(console.error)
},
onReset() {
this.token = null
this.status = null
this.success = null
this.error = null
},
async onInvalidate() {
let data = {
token: this.token
}
const response = await fetch(
`${process.env.VUE_APP_API_URL}guest/invalidate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
}
)
if (response.status === 200) {
this.success = true
this.onReset()
} else {
this.error = await response.json()
}
}

},
}
</script>

<style>
.invalidation-view {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.btn {
padding: 1em;
font-size: 2em;
border: none;
outline: none;
margin: .2em 0;
}
.invalidate-btn {
background: rgb(255, 255, 255);
color: #000;
margin-right: 1em;
}
.reset-btn {
background: rgb(138, 138, 138);
color: #000;
}

</style>

+ 67
- 0
src/views/Reservation.vue View File

@@ -0,0 +1,67 @@
<template>
<div class="qrview" >
<VueQRCodeComponent
v-if="!status || (status && status.valid)"
class="qrcode"
:text="token"
color="#fff"
bg-color="#000"
/>
<div v-if="status && status.valid && status.anzeigename">
{{status.anzeigename}}
</div>
<div v-if="status && !status.valid && status.reason">
reservation {{ token }} from {{status.anzeigename}} is not valid<br>
reason: {{status.reason }}
</div>

</div>
</template>

<script>
import VueQRCodeComponent from 'vue-qrcode-component'
export default {
components: {
VueQRCodeComponent
},
data() {
return {
status: null
}
},
computed: {
token () {
return this.$route.params.token
}
},
methods: {
async getStatus () {
const response = await fetch(`${process.env.VUE_APP_API_URL}guest/${this.token}`)
const data = await response.json()
this.status = data
}
},
mounted () {
this.getStatus()
},
}
</script>

<style>
.qrview {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
min-height: 100vh;
mix-blend-mode: difference;
}
.qrcode > img{
/* padding: 100px; */
/* background-color: #fff; */
display: inline-flex;
width: 400px;
max-width: 80%;
margin: 10%;
}
</style>

+ 287
- 0
src/views/Reservations.vue View File

@@ -0,0 +1,287 @@
<template>
<div class="dark">
<div v-if="reservations" class="stats">
<h1>
{{reservations.length}} active reservations
</h1>
<h2>
{{invalidatedCount}} invalidated<br>
{{validCount}} to go
</h2>
</div>
<div class="guest-table">
<vuetable v-if="reservations"
:fields="fields"
:api-mode="false"
:data="reservations"
:sort-order="sortOrder"
:show-sort-icons="true"
>
<template v-slot:timestamp="props">
<template v-if="props.rowData.timestamp">
{{new Date(props.rowData.timestamp).toLocaleString('de-DE', {day:'numeric',month:'numeric',hour:'numeric',minute:'numeric'})}}
</template>
</template>
<template v-slot:invalidated="props">
<template v-if="props.rowData.invalidated">
{{new Date(props.rowData.invalidated).toLocaleString('de-DE', {day:'numeric',month:'numeric',hour:'numeric',minute:'numeric'})}}
</template>
<button v-else @click="invalidate(props.rowData.token)">invalidate</button>
</template>
<template v-slot:actions="props">
<button @click="cancel(props.rowData.token)">cancel</button>
</template>
<template v-slot:headsup_sent="props">
<template v-if="props.rowData.headsup_sent">
{{new Date(props.rowData.headsup_sent).toLocaleString('de-DE', {day:'numeric',month:'numeric',hour:'numeric',minute:'numeric'})}}
</template>
<button @click="sendHeadsup(props.rowData.id)">X</button>
</template>
</vuetable>
</div>
<label for="showSensitiveData">
<input type="checkbox" name="showSensitiveData" id="showSensitiveData" :value="showSensitiveData" @change="showSensitiveData = !showSensitiveData">
show sensitive data
</label>
<div class="add-guest-form">

<form class="input-form" @submit.prevent="onAddGuest">
<div class="input-headline">add new reservation</div>
<input class="input-text" v-model="anzeigename" placeholder="nickname (public)" />
<input class="input-text" v-model="name" placeholder="real name" />
<input class="input-text" v-model="email" placeholder="email" />
<input class="input-btn" type="submit" :value="(addGuestSending ? 'sending ...' : 'reserve')" :disabled="(addGuestSending ? true : false)" />
<div v-if="addGuestError">
oh no something went wrong!<br>
{{addGuestError.detail}}
</div>
</form>
<button @click="sendHeadsups">SEND ALL EMAILS</button>



</div>
<!-- {{reservations}} -->
</div>
</template>

<script>
import Vuetable from 'vuetable-2'
export default {
name: 'Reservations',
components: {
Vuetable
},
data() {
return {
reservations: null,
sortOrder: [
{
field: 'name',
direction: 'asc',
}
],
showSensitiveData: false,
addGuestSending: false,
addGuestError: null,
anzeigename: '',
name: '',
email: '',
}
},
computed: {
secret () {
return this.$route.params.secret
},
invalidatedCount () {
if (this.reservations && this.reservations.length) {
const filtered = this.reservations.filter(r => !!r.invalidated)
return filtered.length
}
return null
},
validCount () {
if (this.reservations && this.reservations.length) {
const filtered = this.reservations.filter(r => !r.invalidated)
return filtered.length
}
return null
},
fields () {
if (this.showSensitiveData) {
return ['id','timestamp', 'anzeigename', 'name', 'email', 'token', 'invalidated','actions', 'headsup_sent']
} else {
return ['id','timestamp', 'anzeigename', 'invalidated','actions', 'headsup_sent']
}
}
},
methods: {
async getReservations() {
const response = await fetch(`${process.env.VUE_APP_API_URL}guest/management/${this.$route.params.secret}`)
const data = await response.json()
this.reservations = data
},
async cancel(token) {
let data = { token }
const response = await fetch(
`${process.env.VUE_APP_API_URL}guest/cancel`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
}
)
if (response.status === 200) {
this.getReservations()
} else {
alert(await response.json())
}
},
async invalidate(token) {
let data = { token }
const response = await fetch(
`${process.env.VUE_APP_API_URL}guest/invalidate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
}
)
if (response.status === 200) {
this.getReservations()
} else {
alert(await response.json())
}
},

async sendHeadsup(id) {
let data = {
guest_id: id,
secret: this.secret
}
const response = await fetch(
`${process.env.VUE_APP_API_URL}guest/management/send_headsup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
}
)
if (response.status === 200) {
this.getReservations()
} else {
alert(await response.json())
}

},

async sendHeadsups() {
let data = {
secret: this.secret
}
const response = await fetch(
`${process.env.VUE_APP_API_URL}guest/management/send_headsups`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
}
)
if (response.status === 200) {
this.getReservations()
} else {
alert(await response.json())
}

},

async onAddGuest() {
if (this.name.length == 0) {
return false
}
this.addGuestSending = true
let data = {
anzeigename: this.anzeigename,
name: this.name,
email: this.email,
newsletter: false,
}
const response = await fetch(
`${process.env.VUE_APP_API_URL}guest`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}
)
if (response.status === 200) {
this.anzeigename = ''
this.name = ''
this.email = ''
this.getReservations()
} else {
this.getReservations()
this.addGuestError = true
this.addGuestError = await response.json()
}
this.addGuestSending = false
return false
},



},
mounted () {
this.getReservations()
},
}
</script>

<style>
.stats {
display: flex;
flex-direction: column;
align-items: center;
}

.vuetable thead {
background-color: rgb(255, 0, 0);
}

.vuetable-body tr {
border-bottom: 1px dashed rgb(255, 0, 0);
}
.vuetable-body tr:hover {
background-color: rgba(255, 0, 0, 0.281)
}

.vuetable-body td {
padding: .5em;
margin: 0;
}

.guest-table {
display: flex;
flex-direction: column;
align-items: center;
}


.add-guest-form {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 120px
}

button {
background: #fff;
color: #000;
border: none;
text-transform: uppercase;
cursor: pointer;
font-size: 18px;
font-family: inherit;
}
button:hover {
background: #ff0;
color: #000;
}
</style>

+ 36
- 0
src/views/events/gtblank-init/GtBlankDirections.vue View File

@@ -0,0 +1,36 @@
<template>
<div>
<p>
closest station: D-tram station Liechtenwerder Platz<br>
(Station Spittelau [U4, U6, S-Bahn] is also close by)<br>
</p>
<img src="/gt-blank-way0.jpg">
<p>
Take the stairs up to the main entrance "Augasse 2-6".
</p>
<img src="/gt-blank-way1.jpg">
<p>
After entering the building, just turn right and head to the next door (Alteliers, Büro).
</p>
<p>
<a href="geo:48.23201,16.35773">native geo location link</a> |
<a href="https://www.openstreetmap.org/?mlat=48.23201&mlon=16.35773#map=19/48.23201/16.35773" target="_blank">location pin @ osm.org</a>
</p>
</div>
</template>

<script>
export default {

}
</script>

<style>
img {
max-width: 100%;
max-height: 90vh;
}
a {
color: #fff;
}
</style>

+ 315
- 0
src/views/events/gtblank-init/GtblankInit.vue View File

@@ -0,0 +1,315 @@
<template>
<div id="gtblank-init">
<video autoplay muted loop class="video-bg">
<source src="/gt-blank-init-bg.mp4" type="video/mp4">
</video>


<div class="selection-text">{{selectionText}}</div>
<div class="otf">
<!-- <h1 style="padding-top:1em">Party</h1> -->
<div class="form-wrapper">
</div>
<template v-if="status && status.open">
<form v-if="!success" class="input-form" @submit.prevent="onClickTheButton" style="margin-top: 5vh">
<div class="input-headline">gtblank-init 2022-11-11 rsvp</div>
<input class="input-text" v-model="anzeigename" placeholder="nickname (public)" size="1" />
<input class="input-text" v-model="name" placeholder="full name (for orga reasons)" size="1" />
<input class="input-text" v-model="email" placeholder="email (for self service)" size="1" />

<input class="input-btn" type="submit" :value="(sending ? 'sending ...' : 'join')" :disabled="(sending ? true : false)" />
<div v-if="error">
oh no something went wrong!<br>
{{errorReason.detail}}
</div>
</form>
<div v-if="showHelp" style="margin-bottom: 1em;">
*robot voice* give us your data for the reservation!
</div>
<div v-if="success">
<p style="font-size: 6vw">
Thanks!
</p>
<button @click="resetForm" class="input-btn">✓ see you there!</button>
</div>
</template>
<template v-if="status && !status.open">
{{status.reason}}
</template>

<vue-word-cloud class="wordcloud" style="height: 70vh; width: 80%" :words="words"
font-family="'JetBrains Mono'" />
<div v-if="status && status.open" class="details" ref="details">
official start 21:00<br>
augasse 2-6, 1090 wien<br>
<small>
alte wu -> enter main entrance -> turn right to "ateliers" <br>
<a href="https://www.openstreetmap.org/?mlat=48.23201&mlon=16.35773#map=19/48.23201/16.35773" target="_blank">location pin @ osm.org</a>
</small>
</div>
</div>
<!-- :color="() => { return nameColors[Math.floor(Math.random() * nameColors.length)] }" -->
</div>
</template>

<script>
import VueWordCloud from 'vuewordcloud';
export default {
name: 'Invitation',
data() {
return {
anzeigename: '',
name: '',
email: '',
status: null,
newsletter: false,
guests: [],
sending: false,
showHelp: false,
selection: null,
selectionText: null,
success: false,
error: false,
errorReason: null,
}
},
components: {
[VueWordCloud.name]: VueWordCloud,
},
computed: {
nameColors() {
return ['White']
},
words() {
const names = this.guests.map(guest => {
return guest.anzeigename
})
const nameLengths = names.map(name => {
return name.length
})
const maxNameLength = Math.max(...nameLengths)
return this.guests.map(guest => {
const nameLength = guest.anzeigename.length
let size = maxNameLength - nameLength + 12
return [` ${guest.anzeigename} `, size]
})
},
// guestlist() {
// return this.guests.map( (g) => {
// g.name = g.anzeigename
// return g
// })
// }
},
methods: {
async fetchStatus() {
const response = await fetch(`${process.env.VUE_APP_API_URL}status`)
const data = await response.json()
this.status = data
},
async fetchGuests() {
const response = await fetch(`${process.env.VUE_APP_API_URL}guest`)
const data = await response.json()
this.guests = data
},
async onClickTheButton() {
if (this.name.length == 0) {
this.showHelp = true
return false
}
this.showHelp = false
this.sending = true
let data = {
anzeigename: this.anzeigename,
name: this.name,
email: this.email,
newsletter: false,
}
const response = await fetch(
`${process.env.VUE_APP_API_URL}guest`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}
)
if (response.status === 200) {
this.anzeigename = ''
this.name = ''
this.email = ''
this.success = true
this.fetchGuests()
} else {
this.success = false
this.fetchGuests()
this.error = true
this.errorReason = await response.json()
}
this.sending = false
return false
},
resetForm () {
this.anzeigename = ''
this.name = ''
this.email = ''
this.success = false
},
},
mounted() {
this.fetchStatus()
this.fetchGuests()
document.addEventListener('selectionchange', () => {
this.selection = document.getSelection()
// console.log(this.selection)
if (this.selection) {
this.selectionText = this.selection.toString()
}
});
// this.$refs.details.$el
},
}
</script>

<style>

:root {
cursor: url('cursor.png'), auto;
}

#gtblank-init {
background: #999;
}
.video-bg {
position: fixed;
right: 0;
bottom: 0;
min-width: 100%;
min-height: 100%;
}

a {
color: inherit;
cursor: inherit;
}
.otf {
width: 100%;
/* background-color: rgb(167, 204, 247); */
/* background-image: url('bg.gif'); */
background-size: cover;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
position: relative;
}


.input-headline {
color: #000;
mix-blend-mode: multiply;
font-size: 4vh;
margin-bottom: .8vh;
text-align: center;
}

.input-form {
display: flex;
flex-direction: column;
width: 100%;
max-width: 800px;
mix-blend-mode: hard-light;
margin-top: 4vh;
z-index: 9;
position: relative;
}

.input-text,
.input-btn,
.input-cb {
color: #000;
background: #fff;
font-size: 2.8vh;
font-family: 'JetBrains Mono', monospace;
border: 0;
padding: .5em 1em;
line-height: 1.4;
max-width: 100%;
cursor: inherit;
}
.input-btn {
color: #000;
background-color: #01ffcf;
}

.input-btn:hover {
background: #00d9b1;
}
.input-checkbox {
margin: 0 1em 0 0;
}


@media screen and (max-width: 1000px) {
.form-wrapper {
margin-top: 4em;
}

.input-text,
.input-btn {
width: 100%;
max-width: 100%;
min-width: 0;
text-align: center;
}
/* .input-form {
position: absolute;
bottom: 0;
display: flex;
flex-direction: column;
} */
}


.input-text {
width: auto;
flex-grow: 1;
}

.wordcloud {
mix-blend-mode: overlay;
}

.details {
color: #000;
background-color: #fff;
font-size: 30px;
max-width: 80em;
margin: 3% 3% 200px;
padding: 3%;
mix-blend-mode: lighten;
box-shadow: 10px 10px 0 #000fee, -10px -10px 0 #3400ee;
}

.share {
max-width: 30em;
width: 80%;
mix-blend-mode: soft-light;
}
.share:hover {
mix-blend-mode: difference;
}

.selection-text {
position: fixed;
z-index: 9999;
font-size: 10vh;
line-height: 1em;
pointer-events: none;
color: #fff;
mix-blend-mode: difference;
top: 10%;
}
</style>

BIN
View File


+ 322
- 0
src/views/events/okt-28/Okt28.vue View File

@@ -0,0 +1,322 @@
<template>
<div id="okt-28">
<div class="selection-text">{{selectionText}}</div>
<div class="otf" id="teilnehmen">
<!-- <h1 style="padding-top:1em">Party</h1> -->
<div class="form-wrapper">
</div>
<template v-if="status && status.open">
<form v-if="!success" class="input-form" @submit.prevent="onClickTheButton" style="margin-top: 5vh">
<div class="input-headline">reservation</div>
<input class="input-text" v-model="anzeigename" placeholder="nickname (public)" size="1" />
<input class="input-text" v-model="name" placeholder="real name" size="1" />
<input class="input-text" v-model="email" placeholder="email" size="1" />
<!-- <label class="input-cb" for="newsletter">
<input name="newsletter" id="newsletter" type="checkbox" class="input-checkbox" />stay informed
</label> -->
<input class="input-btn" type="submit" :value="(sending ? 'sending ...' : 'reserve')" :disabled="(sending ? true : false)" />
<div v-if="error">
oh no something went wrong!<br>
{{errorReason.detail}}
</div>
</form>
<div v-if="showHelp" style="margin-bottom: 1em;">
*robot voice* give us your data for the reservation!
</div>
<div v-if="success">
<p style="font-size: 6vw">
Thanks!
</p>
<button @click="resetForm" class="input-btn">✓ see you there!</button>
</div>
</template>
<template v-if="status && !status.open">
{{status.reason}}
</template>

<vue-word-cloud class="wordcloud" style="height: 70vh; width: 80%" :words="words"
:color="() => { return nameColors[Math.floor(Math.random() * nameColors.length)] }"
font-family="'JetBrains Mono'" />
</div>
<!-- <div class="details" ref="details">
TEXT!
</div> -->
<div
v-if="canShare"
@click="share"
style="cursor: pointer; margin-top: 5em;"
>
<svg class="share" fill="#fff" version="1.1" viewBox="0 0 350.22 180.68" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-622.81 -392.3)"><path d="m659.49 392.3v16.427h-16.299v19.182h-20.385v50.605h21.942v18.918h17.175v19.63h55.254l-37.054 55.916h30.57l37.054-55.916h190.84v-16.06h15.991v-18.048h18.443v-50.605h-19.999v-20.05h-16.867v-19.998zm6.9343 23.419h263.37v19.998h19.999v41.866h-18.443v16.06h-263.37v-19.63h-21.942v-41.866h20.385z" stroke-width="0" style="paint-order:stroke fill markers"/><g transform="matrix(.42673 0 0 .42673 457.43 276.68)" aria-label="WILLTEILEN"><path d="m590.71 451.07h-21.891l-6.5625-29.859q-0.46875-1.875-1.5-7.4531-0.98437-5.5781-1.4531-9.3281-0.375 3.0469-1.2188 7.5938-0.84375 4.5-1.6875 8.2969-0.79688 3.7969-6.7969 30.75h-21.891l-16.969-68.531h17.859l7.4531 34.359q2.5312 11.391 3.4688 18.141 0.60937-4.7812 2.1562-12.984 1.5938-8.2031 2.9531-13.594l6.0469-25.922h17.156l5.8594 25.922q1.5 6.2344 3.0469 14.391 1.5469 8.1562 2.0625 12.188 0.60937-5.2031 3.3281-18.047l7.5938-34.453h17.859z"/><path d="m616.44 451.07v-68.531h18.609v68.531z"/><path d="m649.86 451.07v-68.531h18.516v53.578h26.391v14.953z"/><path d="m705.74 451.07v-68.531h18.516v53.578h26.391v14.953z"/><path d="m791.85 451.07h-18.516v-53.391h-16.734v-15.141h51.938v15.141h-16.688z"/><path d="m859.02 451.07h-40.688v-68.531h40.688v14.859h-22.172v10.781h20.531v14.859h-20.531v12.938h22.172z"/><path d="m871.02 451.07v-68.531h18.609v68.531z"/><path d="m904.44 451.07v-68.531h18.516v53.578h26.391v14.953z"/><path d="m1001 451.07h-40.687v-68.531h40.687v14.859h-22.172v10.781h20.531v14.859h-20.531v12.938h22.172z"/><path d="m1078.3 451.07h-24.281l-25.031-48.281h-0.4218q0.8906 11.391 0.8906 17.391v30.891h-16.406v-68.531h24.188l24.938 47.625h0.2813q-0.6563-10.359-0.6563-16.641v-30.984h16.5z"/></g></g></svg>
</div>
<a
v-else
href="mailto:?subject=invitation%20-%20oct%2028&body=%40SWK%0D%0Areservation%20required%3A%20reservation.ck.si "
style="cursor: pointer; margin-top: 5em;"
>
<svg class="share" fill="#fff" version="1.1" viewBox="0 0 350.22 180.68" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-622.81 -392.3)"><path d="m659.49 392.3v16.427h-16.299v19.182h-20.385v50.605h21.942v18.918h17.175v19.63h55.254l-37.054 55.916h30.57l37.054-55.916h190.84v-16.06h15.991v-18.048h18.443v-50.605h-19.999v-20.05h-16.867v-19.998zm6.9343 23.419h263.37v19.998h19.999v41.866h-18.443v16.06h-263.37v-19.63h-21.942v-41.866h20.385z" stroke-width="0" style="paint-order:stroke fill markers"/><g transform="matrix(.42673 0 0 .42673 457.43 276.68)" aria-label="WILLTEILEN"><path d="m590.71 451.07h-21.891l-6.5625-29.859q-0.46875-1.875-1.5-7.4531-0.98437-5.5781-1.4531-9.3281-0.375 3.0469-1.2188 7.5938-0.84375 4.5-1.6875 8.2969-0.79688 3.7969-6.7969 30.75h-21.891l-16.969-68.531h17.859l7.4531 34.359q2.5312 11.391 3.4688 18.141 0.60937-4.7812 2.1562-12.984 1.5938-8.2031 2.9531-13.594l6.0469-25.922h17.156l5.8594 25.922q1.5 6.2344 3.0469 14.391 1.5469 8.1562 2.0625 12.188 0.60937-5.2031 3.3281-18.047l7.5938-34.453h17.859z"/><path d="m616.44 451.07v-68.531h18.609v68.531z"/><path d="m649.86 451.07v-68.531h18.516v53.578h26.391v14.953z"/><path d="m705.74 451.07v-68.531h18.516v53.578h26.391v14.953z"/><path d="m791.85 451.07h-18.516v-53.391h-16.734v-15.141h51.938v15.141h-16.688z"/><path d="m859.02 451.07h-40.688v-68.531h40.688v14.859h-22.172v10.781h20.531v14.859h-20.531v12.938h22.172z"/><path d="m871.02 451.07v-68.531h18.609v68.531z"/><path d="m904.44 451.07v-68.531h18.516v53.578h26.391v14.953z"/><path d="m1001 451.07h-40.687v-68.531h40.687v14.859h-22.172v10.781h20.531v14.859h-20.531v12.938h22.172z"/><path d="m1078.3 451.07h-24.281l-25.031-48.281h-0.4218q0.8906 11.391 0.8906 17.391v30.891h-16.406v-68.531h24.188l24.938 47.625h0.2813q-0.6563-10.359-0.6563-16.641v-30.984h16.5z"/></g></g></svg>
</a>
</div>
</template>

<script>
import VueWordCloud from 'vuewordcloud';
export default {
name: 'Invitation',
data() {
return {
anzeigename: '',
name: '',
email: '',
status: null,
newsletter: false,
guests: [],
sending: false,
showHelp: false,
selection: null,
selectionText: null,
success: false,
error: false,
errorReason: null,
}
},
components: {
[VueWordCloud.name]: VueWordCloud,
},
computed: {
nameColors() {
return ['DeepPink', 'Crimson', 'Blue', 'BlueViolet', 'Fuchsia', 'Navy', 'Indigo']
},
words() {
const names = this.guests.map(guest => {
return guest.anzeigename
})
const nameLengths = names.map(name => {
return name.length
})
const maxNameLength = Math.max(...nameLengths)
return this.guests.map(guest => {
const nameLength = guest.anzeigename.length
let size = maxNameLength - nameLength + 12
return [` ${guest.anzeigename} `, size]
})
},
// guestlist() {
// return this.guests.map( (g) => {
// g.name = g.anzeigename
// return g
// })
// }
canShare () {
return !!navigator.share
}
},
methods: {
async fetchStatus() {
const response = await fetch(`${process.env.VUE_APP_API_URL}status`)
const data = await response.json()
this.status = data
},
async fetchGuests() {
const response = await fetch(`${process.env.VUE_APP_API_URL}guest`)
const data = await response.json()
this.guests = data
},
async onClickTheButton() {
if (this.name.length == 0) {
this.showHelp = true
return false
}
this.showHelp = false
this.sending = true
let data = {
anzeigename: this.anzeigename,
name: this.name,
email: this.email,
newsletter: false,
}
const response = await fetch(
`${process.env.VUE_APP_API_URL}guest`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}
)
if (response.status === 200) {
this.anzeigename = ''
this.name = ''
this.email = ''
this.success = true
this.fetchGuests()
} else {
this.success = false
this.fetchGuests()
this.error = true
this.errorReason = await response.json()
}
this.sending = false
return false
},
resetForm () {
this.anzeigename = ''
this.name = ''
this.email = ''
this.success = false
},
async share () {
location.href = "#";
try {
await navigator.share({ url:"https://reservation.ck.si/" })
} catch (err) {
console.log(`Error: ${err}`)
}
}
},
mounted() {
this.fetchStatus()
this.fetchGuests()
document.addEventListener('selectionchange', () => {
this.selection = document.getSelection()
// console.log(this.selection)
if (this.selection) {
this.selectionText = this.selection.toString()
}
});
// this.$refs.details.$el
},
}
</script>

<style>


body {
font-family: 'JetBrains Mono', monospace;
background-color: rgb(0, 0, 0);
background-image: url('bg.gif');
background-size: cover;
background-position: center center;
background-attachment: fixed;
color: rgb(251, 251, 251);
margin: 0 0 100px;
}

.otf {
width: 100%;
/* background-color: rgb(167, 204, 247); */
/* background-image: url('bg.gif'); */
background-size: cover;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
position: relative;
}


.input-headline {
color: rgb(255, 255, 255);
font-size: 2.8vh;
margin-bottom: .8vh;
}

.input-form {
display: flex;
flex-direction: column;
width: 100%;
max-width: 800px;
mix-blend-mode: exclusion;
margin-top: 4vh;
z-index: 9;
position: relative;
}

.input-text,
.input-btn,
.input-cb {
color: #000;
background: #fff;
font-size: 2.8vh;
font-family: 'JetBrains Mono', monospace;
border: 0;
padding: .5em 1em;
line-height: 1.4;
max-width: 100%;
}
.input-btn {
color: #fff;
background-color: #1e3a8e;
border-top: 2px solid rgb(0, 0, 0);
cursor: pointer;
}

.input-btn:hover {
color: #fff;
background: #3260eb;
}

.input-checkbox {
margin: 0 1em 0 0;
}


@media screen and (max-width: 1000px) {
.form-wrapper {
margin-top: 4em;
}

.input-text,
.input-btn {
width: 100%;
max-width: 100%;
min-width: 0;
text-align: center;
}
/* .input-form {
position: absolute;
bottom: 0;
display: flex;
flex-direction: column;
} */
}


.input-text {
width: auto;
flex-grow: 1;
}



.details {
color: #fff;
background-color: #000;
font-size: 10vw;
max-width: 80em;
margin: 3% 3% 200px;
padding: 3%;
}

.share {
max-width: 30em;
width: 80%;
mix-blend-mode: soft-light;
}
.share:hover {
mix-blend-mode: difference;
}

.selection-text {
position: fixed;
z-index: 9999;
font-size: 10vh;
line-height: 1em;
pointer-events: none;
color: #ec4b73;
mix-blend-mode: multiply;
top: 10%;
}
</style>

Loading…
Cancel
Save