Initial commit

This commit is contained in:
Grit-Grenzdoerfer
2023-09-17 13:24:39 +02:00
commit 5f27fe0c5b
753 changed files with 15517 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
name: banner
uploadPath: ../media/banner
meta:
label: Banner
muiIcon: web
views:
- type: table
columns:
- source: banner
permissions:
public:
methods:
get: true
post: false
put: false
delete: false
user:
methods:
get: true
post: true
put: true
delete: true
fields:
- name: banner
type: string
meta:
label: Banner

122
api/collections/content.yml Normal file
View File

@@ -0,0 +1,122 @@
name: content
uploadPath: ../media/content
meta:
label: Inhalt
muiIcon: web
views:
- type: table
columns:
- source: path
tablist:
activeTab: site
tabs:
- name: general
label: Allgemein
subFields:
- source: path
- name: teaser
label: Teaser
subFields:
- source: showTeaser
- source: teaserTitle
- source: teaserDescription
- source: teaserImages
- name: site
label: content
subFields:
- source: row
imageFilter:
xs:
- fit: true
height: 90
width: 90
resampling: lanczos
quality: 60
s:
- fit: true
height: 300
width: 300
resampling: lanczos
quality: 60
m:
- fit: true
height: 600
width: 600
resampling: lanczos
quality: 60
l:
- fit: true
height: 1240
width: 1240
resampling: lanczos
quality: 60
xl:
- fit: true
height: 2000
width: 2000
resampling: lanczos
quality: 60
permissions:
public:
methods:
get: true
post: false
put: false
delete: false
user:
methods:
get: true
post: true
put: true
delete: true
projections:
navigation:
select:
path: 1
fields:
- type: string
name: path
meta:
label: Pfad
helperText: "Ein Pfad sollte mit einem / starten und ohne eins enden."
- name: showTeaser
type: boolean
meta:
label: Anzeigen
helperText: "Ist dies aktiviert, so wird der Teaser in der Startseite angezeigt."
- type: object[]
name: teaserImages
meta:
label: Bider
addElementLabel: Bild Hinzufügen
helperText: "Bei mehreren Bildern wird ein Slider eingefügt."
dependsOn:
eval: $parent.showTeaser
subFields:
- name: image
type: file
meta:
label: Bild
- type: string
name: teaserTitle
meta:
label: Titel
dependsOn:
eval: $parent.showTeaser
- type: string
name: teaserDescription
meta:
label: Beschreibung
dependsOn:
eval: $parent.showTeaser
- !include fields/pagebuilder.yml

View File

@@ -0,0 +1,731 @@
type: object[]
name: row
meta:
label: Zeile
addElementLabel: Zeile hinzufügen
folding:
previewFolded: titleForWork
previewUnfolded: titleForWork
subFields:
- name: title
type: string
meta:
label: Titel
- !include titleForWork.yml
- name: maxWidth
type: boolean
meta:
label: Maximale Breite
- name: iconBackgroundImage
type: boolean
meta:
label: Hintergrund Wellen
- name: iconBackgroundTitle
type: boolean
meta:
label: Titel Highlights des Sees hinzufügen.
- name: noGap
type: boolean
meta:
label: Keine Lücken
- name: column
type: object[]
meta:
label: Spalte
addElementLabel: weitere Spalte
folding:
previewFolded: contentType
previewUnfolded: contentType
css: &cols
input:
wrapper: |
& > div > div {
display: flex;
flex-direction: row;
align-items: baseline;
flex-wrap: wrap;
padding-right: 40px;
}
& .add-element {
width: 30px;
min-height: 200px;
& > button {
transform-origin: 0 0;
transform: rotate(90deg) translateY(-100%);
justify-content: left;
width: 200px;
}
}
element:
eval: |
`
flex: 1;
flex-grow: ${{"": 1, narrow: 0.5, medium: 1, wide: 2}[$element?.width]};
min-width: 350px;
height: 100%;
`
subFields:
- name: contentType
type: string
meta:
label: ""
widget: select
choices:
- name: Hauptbild
id: mainPicture
- name: Bild
id: image
- name: Tabelle
id: table
- name: Informatiosübersicht
id: infoBoard
- name: Iconübersicht
id: iconBoard
- name: Formular
id: form
- name: Vieoswitch
id: videos
- name: Textfeld
id: text
- name: GoogleMaps
id: googleMaps
- name: Video
id: video
- name: imageSlider
type: object[]
meta:
label: Bild
helperText: Wird nur ein Bild angegeben, so wird kein Slider verwendet.
dependsOn:
eval: $parent.contentType == "image"
subFields:
- name: image
type: file
meta:
label: Bild
widget: image
- name: mainPicture
type: file
meta:
widget: image
label: Hauptbild
dependsOn:
eval: $parent.contentType == "mainPicture"
- name: showGoogleMaps
type: boolean
meta:
label: Anzeigen
dependsOn:
eval: $parent.contentType == "googleMaps"
- name: inscription
type: string
meta:
widget: text
label: Aufschrift
dependsOn:
eval: $parent.contentType == "mainPicture"
- name: textFieldHeading
type: string
meta:
label: Überschrift
dependsOn:
eval: $parent.contentType == "text"
- name: textContent
type: string
meta:
label: Text
widget: richtext
dependsOn:
eval: $parent.contentType == "text"
- name: siteReference
type: boolean
meta:
label: Zeilenreferenzen
helperText: Für die zweite Spalte neben Hauptbild empfohlen, refereziert alle Reihen auf der folgenden Seite.
dependsOn:
eval: $parent.contentType == "text"
- name: tableFieldHeading
type: string
meta:
label: Überschrift
dependsOn:
eval: $parent.contentType == "table"
- name: annotation
type: string
meta:
label: Anmerkung
dependsOn:
eval: $parent.contentType == "table" || $parent.contentType == "form"
- name: table
type: object[]
meta:
label: Tabelle
addElementLabel: Tabelle Hinzufügen
folding:
previewFolded: titleForWork
previewUnfolded: titleForWork
dependsOn:
eval: $parent.contentType == "table"
css:
input:
wrapper: |
& .object-array-input-content{
& .add-element {
height: 30px;
min-height: 0px;
width: 100%;
& > button {
transform-origin: 0 0;
transform: rotate(0deg) translateY(0);
}
}
}
subFields:
- !include titleForWork.yml
- !include title.yml
- name: hintsTable
type: string
meta:
label: Hinweise
widget: richtext
- name: tableRow
type: object[]
meta:
label: Zeile
addElementLabel: Zeile hinzufügen
css:
input:
wrapper: |
& .object-array-input-content{
& .add-element {
height: 30px;
min-height: 0px;
width: 100%;
& > button {
transform-origin: 0 0;
transform: rotate(0deg) translateY(0);
}
}
}
subFields:
- name: bold
type: boolean
meta:
label: Dick
- name: left
type: string
meta:
label: Linke Seite
- name: center
type: string
meta:
label: Mitte
- name: right
type: string
meta:
label: Rechte Seite
- name: hintsTable
type: string
meta:
label: Hinweise
widget: richtext
dependsOn:
eval: $parent.contentType == "table"
- name: siteRefs
type: boolean
meta:
dependsOn:
eval: $parent.contentType == "table"
label: Zeilenreferenzen
helperText: Für die zweite Spalte neben Hauptbild empfohlen, refereziert alle Reihen auf der folgenden Seite.
- name: image
type: file
meta:
label: Bild
widget: image
dependsOn:
eval: $parent.contentType == "infoBoard"
- name: title
type: string
meta:
label: Titel
dependsOn:
eval: $parent.contentType == "infoBoard"
- name: text
type: string
meta:
label: description
widget: richtext
dependsOn:
eval: $parent.contentType == "infoBoard"
- name: links
type: object[]
meta:
label: Links
dependsOn:
eval: $parent.contentType == "infoBoard"
subFields:
- name: name
type: string
meta:
label: Name
- name: site
type: string
meta:
label: Seite
widget: select
choices:
endpoint: content
mapping:
id: path
name: path
- name: iconBoard
type: object[]
meta:
label: Icons
css:
input:
wrapper: |
& .object-array-input-content{
display: flex;
flex-wrap: wrap;
}
element: |
& {
flex-grow: 1;
min-width: 33%;
marin-top: 0px !important;
height: 100%;
}
dependsOn:
eval: $parent.contentType == "iconBoard"
subFields:
- name: icon
type: file
meta:
widget: image
label: Icon
- name: subText
type: string
meta:
label: Text
- name: video
type: file
meta:
widget: file
dependsOn:
eval: $parent.contentType == "video"
- name: titleVideo
type: string
meta:
label: Titel
dependsOn:
eval: $parent.contentType == "video"
- name: descriptionVideo
type: string
meta:
label: Beschreibung
widget: richtext
dependsOn:
eval: $parent.contentType == "video"
- name: videoSwitch
type: object[]
validator:
eval: |
(function(){
if($parent.contentType != "videos") return true;
return $parent.videoSwitch.length == 2;
})()
meta:
label: Videoswitch
helperText: Hier sind 2 Angaben notwendig!
dependsOn:
eval: $parent.contentType == "videos"
folding:
previewFolded: titleForWork
previewUnfolded: titleForWork
css:
input:
wrapper: |
& .object-array-input-content{
display: flex;
width: 100%;
}
element: |
& {
flex-grow: 1;
margin-top: 0px !important;
height: 100%;
}
subFields:
- name: video
type: file
meta:
widget: file
- !include titleForWork.yml
- name: title
type: string
meta:
label: Titel
- name: description
type: string
meta:
widget: richtext
label: Beschreibung
- name: link
type: string
meta:
label: link
widget: select
choices:
endpoint: content
mapping:
id: path
name: path
- name: formEmailTitle
type: string
meta:
label: Formular Email Titel
dependsOn:
eval: $parent.contentType == "form"
- name: formRows
type: object[]
meta:
label: Zeile
addElementLabel: Zeile Hinzufügen
dependsOn:
eval: $parent.contentType == "form"
subFields:
- name: rowName
type: string
meta:
label: Zeilenname
- name: columns
type: object[]
meta:
label: Spalte
addElementLabel: Spalte hinzufügen
folding:
previewFolded: titleForWork
previewUnfolded: titleForWork
css: *cols
subFields:
- !include titleForWork.yml
- name: title
type: string
meta:
label: Überschrift
helperText: Optional
- name: showLabelNumber
type: boolean
meta:
label: Label Nummerinput Anzeigen
- name: labelNumber
type: object[]
meta:
label: Nummber block
dependsOn:
eval: $parent?.showLabelNumber
subFields:
- name: group
type: number
meta:
label: Gruppe
- name: title
type: string
meta:
label: Titel
- name: emailName
type: string
meta:
label: Email Name
- name: block
type: object[]
meta:
label: Block
subFields:
- name: label
type: string
meta:
label: Label
- name: emailName
type: string
meta:
label: Email Name
- name: showTimes
type: boolean
meta:
label: Zeitenauswahlfeld Anzeigen
- name: times
type: object[]
meta:
label: Zeitenangabe
helperText: "Die Angaben werden in folgendes Übersetzt: Anfangspunkt - Endpunkt"
dependsOn:
eval: $parent?.showTimes
css: &timesCss
input:
wrapper: |
& .object-array-input-content {
.add-element{
transform-origin: right;
margin-left: 80px;
transform: rotate(-90deg);
}
.object .sliceFields{
display: flex;
.field-input-wrapper{
width: 50%;
}
}
}
subFields:
- name: timeFrom
type: string
meta:
label: Anfangspunkt
helperText: Bspw. 14:30
- name: timeTo
type: string
meta:
label: Endpunkt
helperText: Bspw. 15:30
- name: timesPlaceholder
type: string
meta:
label: Platzhalter im leeren Eingabefeld
dependsOn:
eval: $parent?.showTimes
- name: timesfieldOrder
type: number
meta:
label: Reihenfolge
helperText: Die kleinste angegebene Zahl wird am weitesten oben in der Formularspalte stehen
dependsOn:
eval: $parent?.showTimes
- name: dateSelectNotRequired
type: boolean
meta:
label: nicht Notwendig
dependsOn:
eval: $parent?.showTimes
- name: emailNameTimes
type: string
meta:
label: Email Name
dependsOn:
eval: $parent?.showTimes
- name: showSelect
type: boolean
meta:
label: Auswahlfeld anzeigen
- name: selectTitle
type: string
meta:
label: Select Placeholdertext
dependsOn:
eval: $parent.showSelect
- name: selectEntries
type: object[]
meta:
label: Auswahleingabe
helperText: "Die Angaben werden in folgendes Übersetzt: Anfangspunkt - Endpunkt"
dependsOn:
eval: $parent.showSelect
css: *timesCss
subFields:
- name: leftSide
type: string
meta:
label: Anfangspunkt
helperText: Bspw. 14:30
- name: rightSide
type: string
meta:
label: Endpunkt
helperText: Bspw. 15:30
- name: timeNotRequired
type: boolean
meta:
label: nicht Notwendig
dependsOn:
eval: $parent?.showSelect
- name: selectPlaceholder
type: string
meta:
label: Platzhalter im leeren Eingabefeld
dependsOn:
eval: $parent?.showSelect
- name: emailNameTime
type: string
meta:
label: Email Name
dependsOn:
eval: $parent?.showSelect
- name: showDate
type: boolean
meta:
label: Datumsauswahl
- name: datePlaceholder
type: string
meta:
label: Datum Platzhalter für das leere Eingabefeld
dependsOn:
eval: $parent?.showDate
- name: datefieldOrder
type: number
meta:
label: Reihenfolge
helperText: Die kleinste angegebene Zahl wird am weitesten oben in der Formularspalte stehen
dependsOn:
eval: $parent?.showDate
- name: dateNotRequired
type: boolean
meta:
label: nicht Notwendig
dependsOn:
eval: $parent?.showDate
- name: emailNameDate
type: string
meta:
label: Email Name
dependsOn:
eval: $parent?.showDate
- name: showNumber
type: boolean
meta:
label: Nummerfeld
- name: numberPlaceholder
type: string
meta:
label: Nummer Platzhalter
dependsOn:
eval: $parent?.showNumber
- name: numberfieldOrder
type: number
meta:
label: Reihenfolge
helperText: Die kleinste angegebene Zahl wird am weitesten oben in der Formularspalte stehen
dependsOn:
eval: $parent?.showNumber
- name: numberNotRequired
type: boolean
meta:
label: nicht Notwendig
dependsOn:
eval: $parent?.showNumber
- name: emailNameNumber
type: string
meta:
label: Email Name
dependsOn:
eval: $parent?.showNumber
- name: text
type: object[]
meta:
label: Textfeld
addElementLabel: Textfeld hinzufügen
folding:
previewFolded: titleForWork
previewUnfolded: titleForWork
subFields:
- !include titleForWork.yml
- name: textPlaceholder
type: string
meta:
label: Platzhalter für das leere Eingabefeld
- name: textArea
type: boolean
meta:
label: Großes Textfeld
- name: emailValidation
type: boolean
meta:
label: E-Mail-Validierung
- name: telValidation
type: boolean
meta:
label: Telefon-Validierung
- name: notRequired
type: boolean
meta:
label: nicht Notwendig
- name: emailName
type: string
meta:
label: Email Name
- name: textfieldOrder
type: number
meta:
label: Reihenfolge
helperText: Die kleinste angegebene Zahl wird am weitesten oben in der Formularspalte stehen

View File

@@ -0,0 +1,4 @@
name: title
type: string
meta:
label: Titel

View File

@@ -0,0 +1,4 @@
name: titleForWork
type: string
meta:
label: Orientierungstitel

40
api/collections/forms.yml Normal file
View File

@@ -0,0 +1,40 @@
name: forms
uploadPath: ../media/forms
meta:
label: Formulare
muiIcon: web
views:
- type: table
columns:
- source: formular
permissions:
public:
methods:
get: true
post: true
put: false
delete: false
user:
methods:
get: true
post: true
put: true
delete: true
hooks:
post:
create:
type: javascript
file: hooks/forms/post_create.js
return:
type: javascript
file: hooks/forms/post_return.js
fields:
- type: object
name: formular
meta:
label: Formular
widget: jsonField

View File

@@ -0,0 +1,134 @@
name: navigation
uploadPath: ../media/navigation
meta:
label: "Navigation"
muiIcon: navigation
views:
- type: simpleList
mediaQuery: "(max-width:599px)"
primaryText: tree
- type: table
mediaQuery: "(min-width:600px)"
columns:
- source: tree
permissions:
public:
methods:
get: true
post: false
put: false
delete: false
user:
methods:
get: true
post: false
put: true
delete: false
x-endpoint: &endpoint
name: endpoint
type: boolean
meta:
defaultValue:
eval: 1 == 1
label: Endpunkt
x-elemente: &elemente
name: elemente
type: object[]
meta:
label:
de: Elemente
en: elements
folding:
previewUnfolded: name
previewFolded: name
x-name: &name
name: name
type: string
meta:
label:
de: Name
en: name
helperText: Dieser Name wird zur Anzeige in der Navigation verwendet.
x-seite: &seite
name: seite
type: string
meta:
label:
de: Seite
en: page
widget: select
dependsOn:
eval: |
(function(){
if($parent.endpoint == undefined) return true;
return $parent.endpoint
})()
choices:
endpoint: content
params:
sort: path
projection: navigation
mapping:
id: id
name: path
fields:
- name: tree
type: number
meta:
label: Baum
widget: select
helperText: Die Servicenavigation sollte Seiten wie bspw. die Datneschutzerklärung oder das Impressum umfassen.
choices:
- id: 0
name:
de: Hauptnavigation
en: main navigation
- id: 1
name:
de: Servicenavigation
en: service navigation
- <<: *elemente
subFields:
- *endpoint
- *name
- *seite
- name: image
type: file
meta:
label: Bild
dependsOn:
eval: |
(function(){
if($parent.endpoint == undefined) return false;
return !$parent.endpoint
})()
- name: elemente
type: object[]
meta:
label:
de: Elemente
en: elements
dependsOn:
eval: |
(function(){
if($parent.endpoint == undefined) return false;
return !$parent.endpoint
})()
folding:
previewUnfolded: name
previewFolded: name
subFields:
- *name
- *seite

62
api/collections/ssr.yml Normal file
View File

@@ -0,0 +1,62 @@
########################################################################
# SSR Dummy collections
########################################################################
name: ssr
meta:
label: { de: "SSR Dummy", en: "ssr dummy" }
muiIcon: server
rowIdentTpl: { twig: "{{ id }}" }
views:
- type: simpleList
mediaQuery: "(max-width: 600px)"
primaryText: id
secondaryText: insertTime
tertiaryText: path
- type: table
columns:
- id
- insertTime
- path
permissions:
public:
methods:
get: false
post: false
put: false
delete: false
user:
methods:
get: false
post: false
put: false
delete: false
"token:${SSR_TOKEN}":
methods:
# only via url=
get: true
post: true
put: false
delete: false
hooks:
get:
read:
type: javascript
file: hooks/ssr/get_read.js
post:
bind:
type: javascript
file: hooks/ssr/post_bind.js
# we only need hooks
fields:
- name: path
type: string
index: [single, unique]
- name: content
type: string
meta:
inputProps:
multiline: true

View File

@@ -0,0 +1,30 @@
name: temperature
uploadPath: ../media/temperature
meta:
label: Temperatur
views:
- type: table
columns:
- source: temperature
permissions:
public:
methods:
get: true
post: false
put: false
delete: false
user:
methods:
get: true
post: false
put: false
delete: false
fields:
- name: temperature
type: number
meta:
label: Temperatur
helperText: do not modify, will be modified automatically hourly.

19
api/config.yml Normal file
View File

@@ -0,0 +1,19 @@
namespace: wasserski_erfurt
meta:
openapi:
servers:
- url: https://tibi-admin-server.code.testversion.online/api/v1/_/demo
description: code-server
collections:
- !include collections/navigation.yml
- !include collections/content.yml
- !include collections/banner.yml
- !include collections/forms.yml
- !include collections/temperature.yml
jobs:
- cron: "0 * * * *"
type: javascript
file: jobs/requestTemperature.js

1
api/config.yml.env Normal file
View File

@@ -0,0 +1 @@
TOKEN=geheim

View File

@@ -0,0 +1,10 @@
const release = "tibi-docs.dirty"
// @ts-ignore
if (release && typeof context !== "undefined") {
context.response.header("X-Release", release)
}
module.exports = {
release,
}

27
api/hooks/config.js Normal file
View File

@@ -0,0 +1,27 @@
module.exports = {
ssrValidatePath: function (path) {
// validate if path ssr rendering is ok, -1 = NOTFOUND, 0 = NO SSR, 1 = SSR
// pe. use context.readCollection("product", {filter: {path: path}}) ... to validate dynamic urls
// / is de home
if (path == "/") return 1
// all other sites are in db
path = path?.replace(/^\//, "")
// filter for path or alternativePaths
const resp = context.db.find("content", {
filter: {
$or: [{ path }, { "alternativePaths.path": path }],
},
selector: { _id: 1 },
})
if (resp && resp.length) {
return 1
}
// not found
return -1
},
ssrAllowedAPIEndpoints: ["content", "medialib"],
}

View File

@@ -0,0 +1,14 @@
const { validateFields } = require("./validateFields")
;(function () {
if (context.data.formular.honey) {
throw { status: 400, error: "Bot detection" }
}
delete context.data.formular.honey
let values = Object.entries(context.data.formular)
let validation = validateFields(values)
console.log(validation)
if (validation.length) {
throw { status: 400, error: validation }
}
})()

View File

@@ -0,0 +1,60 @@
var utils = require("../lib/utils")
var config = require("../config")
;(function () {
let formular = context.data.formular
let tempForm = {}
let formTitle = `${formular.formTitle}`
delete formular.formTitle
delete formular["agreement"]
delete formular["honey"]
formular.formRows.forEach((rowName, i) => {
tempForm[rowName] = {}
})
let getValue = (e) => {
if (e[0].includes("numberLabel")) return e[1][0]
return e[1]
}
let indices = {}
Object.entries(formular).forEach((e) => {
if (e[0] == "formRows") return
let key = e[0].split("_")
let newKey = key[key.length - 1]
let rowName = formular.formRows[Number(key[key.length - 2])]
let index = isNaN(Number(key[key.length - 3])) ? 100 : Number(key[key.length - 3])
if (!rowName) return
if (!tempForm[rowName]) tempForm[rowName] = {}
if (tempForm[rowName][newKey]) {
tempForm[rowName][newKey][0] += "\\" + getValue(e)
} else {
tempForm[rowName][newKey] = [getValue(e), newKey[0] == "n", newKey.includes("invalid")]
indices[rowName] = { ...indices[rowName], [newKey]: index }
}
})
// Now iterate over tempForm to sort fields in each row
Object.entries(tempForm).forEach(([rowName, fields]) => {
tempForm[rowName] = Object.entries(fields)
.sort((a, b) => indices[rowName][a[0]] - indices[rowName][b[0]])
.reduce((acc, [key, val]) => ({ ...acc, [key]: val }), {})
})
console.log(JSON.stringify(tempForm), JSON.stringify(indices))
Object.keys(tempForm).forEach((row) => {
tempForm[row] = Object.entries(tempForm[row])
})
delete tempForm[undefined]
context.smtp.sendMail({
to: "info@wasserski-erfurt.de",
from: "mail@webmakers.de",
subject: "Wasserski " + formTitle,
html: context.tpl.execute(context.fs.readFile("templates/form_mail.html"), {
context: context,
formularRows: Object.entries(tempForm),
formTitle: formTitle,
}),
})
})()

View File

@@ -0,0 +1,67 @@
function validateFields(fieldsArray) {
const errors = []
let selectedGroup
const numberRegex = /^[+]?([.]\d+|\d+([.]\d+)?)$/
const emailRegex =
/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/
const dateRegex = /^\d{4}\-(0?[1-9]|1[012])\-(0?[1-9]|[12][0-9]|3[01])$/
const timeRegex = /^\d{1,2}:\d{2}-\d{1,2}:\d{2}$/
const phoneRegex = /^\s*(?:\+?(\d{1,3}))?[-. (]*(\d{3})[-. )]*(\d{3})[-. ]*(\d{4})(?: *x(\d+))?\s*$/
const wholeBlockInvalid = () => {
const blockContainer = document.getElementsByClassName("blockContainer")[0]
blockContainer.classList.add("invalidBlocks")
}
const validateNumber = (value, field, element) => {
if (!numberRegex.test(`${value}`)) {
errors.push(["block", () => element.classList.add("border-red")])
}
}
fieldsArray.forEach(([field, value]) => {
if (field === "blockGroups" || field.includes("numberLabel")) {
if (!field.includes("numberLabel")) return
const [elementValue, element, group] = value
if (!elementValue) return
if (selectedGroup !== undefined) {
if (group !== selectedGroup) {
errors.push(["block", wholeBlockInvalid])
} else {
validateNumber(elementValue, field, element)
}
} else {
selectedGroup = group
validateNumber(elementValue, field, element)
}
return
}
if (!value) {
errors.push(["Eingabe ist erforderlich.", field])
} else if (field.includes("number_")) {
if (!numberRegex.test(`${value}`)) {
errors.push(["Ungültiger numerischer Wert.", field])
}
} else if (field.includes("agreement_")) {
if (value !== true) errors.push(["Bitte das Kontrollkästchen anklicken.", field])
} else if (field.includes("Email_")) {
if (!emailRegex.test(value)) errors.push(["Ungültiges E-Mail-Format.", field])
} else if (field.includes("date_")) {
if (!dateRegex.test(value)) errors.push(["Ungültiges Datumsformat.", field])
} else if (field.includes("times_")) {
if (!timeRegex.test(value)) errors.push(["Ungültiges Zeitformat.", field])
} else if (field.includes("Telefon_")) {
if (!phoneRegex.test(value)) errors.push(["Ungültiges Telefonnummernformat.", field])
}
})
return errors
}
module.exports = {
validateFields,
}

55
api/hooks/lib/utils.js Normal file
View File

@@ -0,0 +1,55 @@
/**
*
* @param {any} str
*/
function log(str) {
console.log(JSON.stringify(str, undefined, 4))
}
/**
* convert object to string
* @param {any} obj object
*/
function obj2str(obj) {
if (Array.isArray(obj)) {
return JSON.stringify(
obj.map(function (idx) {
return obj2str(idx)
})
)
} else if (typeof obj === "object" && obj !== null) {
var elements = Object.keys(obj)
.sort()
.map(function (key) {
var val = obj2str(obj[key])
if (val) {
return key + ":" + val
}
})
var elementsCleaned = []
for (var i = 0; i < elements.length; i++) {
if (elements[i]) elementsCleaned.push(elements[i])
}
return "{" + elementsCleaned.join("|") + "}"
}
if (obj) return obj
}
/**
* clear SSR cache
*/
function clearSSRCache() {
var info = context.db.deleteMany("ssr", {})
context.response.header("X-SSR-Cleared", info.removed)
}
module.exports = {
log,
clearSSRCache,
obj2str,
}

183
api/hooks/ssr/get_read.js Normal file
View File

@@ -0,0 +1,183 @@
const { ssrValidatePath, ssrAllowedAPIEndpoints } = require("../config")
const { obj2str, log } = require("../lib/utils")
;(function () {
/** @type {HookResponse} */
var response = null
var request = context.request()
var url = request.query("url")
var noCache = request.query("noCache")
// add sentry trace id to head
var trace_id = context.debug.sentryTraceId()
function addSentryTrace(content) {
return content.replace("</head>", '<meta name="sentry-trace" content="' + trace_id + '" /></head>')
}
context.response.header("sentry-trace", trace_id)
if (url) {
// comment will be printed to html later
var comment = ""
url = url.split("?")[0]
comment += "url: " + url
if (url && url.length > 1) {
url = url.replace(/\/$/, "")
}
if (url == "/noindex" || !url) {
url = "/" // see .htaccess
}
// check if url is in cache
var cache =
!noCache &&
context.db.find("ssr", {
filter: {
path: url,
},
})
if (cache && cache.length) {
// use cache
throw {
status: 200,
log: false,
html: addSentryTrace(cache[0].content),
}
}
// validate url
var status = 200
var pNorender = false
var pNotfound = false
var pR = ssrValidatePath(url)
if (pR < 0) {
pNotfound = true
} else if (!pR) {
pNorender = true
}
var head = ""
var html = ""
var error = ""
comment += ", path: " + url
var cacheIt = false
if (pNorender) {
html = "<!-- NO SSR RENDERING -->"
} else if (pNotfound) {
status = 404
html = "404 NOT FOUND"
} else {
// try rendering, if error output plain html
try {
// @ts-ignore
context.ssrCache = {}
// @ts-ignore
context.ssrFetch = function (endpoint, options) {
var data
if (ssrAllowedAPIEndpoints.indexOf(endpoint) > -1) {
var _options = Object.assign({}, options)
if (_options.sort) _options.sort = [_options.sort]
try {
/*console.log(
"SSR",
endpoint,
JSON.stringify(_options)
)*/
var goSlice = context.db.find(endpoint, _options || {})
// need to deep copy, so shift and delete on pure js is possible
data = JSON.parse(JSON.stringify(goSlice))
} catch (e) {
console.log("ERROR", JSON.stringify(e))
data = []
}
} else {
console.log("SSR forbidden", endpoint)
data = []
}
var count = (data && data.length) || 0
if (options && count == options.limit) {
// read count from db
count = context.db.count(endpoint, _options || {})
}
var r = { data: data, count: count }
// @ts-ignore
context.ssrCache[obj2str({ endpoint: endpoint, options: options })] = r
return r
}
// include App.svelte and render it
// @ts-ignore
var app = require("../lib/app.server")
var rendered = app.default.render({
url: url,
})
head = rendered.head
html = rendered.html
// add ssrCache to head
head +=
"\n\n" +
"<script>window.__SSR_CACHE__ = " +
// @ts-ignore
JSON.stringify(context.ssrCache) +
"</script>"
// status from webapp
// @ts-ignore
if (context.is404) {
status = 404
} else {
cacheIt = true
}
} catch (e) {
// save error for later insert into html
log(e.message)
log(e.stack)
error = "error: " + e.message + "\n\n" + e.stack
}
}
// read html template and replace placeholders
var tpl = context.fs.readFile("templates/spa.html")
tpl = tpl.replace("<!--HEAD-->", head)
tpl = tpl.replace("<!--HTML-->", html)
tpl = tpl.replace("<!--SSR.ERROR-->", error ? "<!--" + error + "-->" : "")
tpl = tpl.replace("<!--SSR.COMMENT-->", comment ? "<!--" + comment + "-->" : "")
// save cache if adviced
if (cacheIt && !noCache) {
context.db.create("ssr", {
path: url,
content: tpl,
})
}
// return html
throw {
status: status,
log: false,
html: addSentryTrace(tpl),
}
} else {
// only admins are allowed to get without url parameter
var auth = context.user.auth()
if (!auth || auth.role !== 0) {
throw {
status: 403,
message: "invalid auth",
auth: auth,
}
}
}
})()

View File

@@ -0,0 +1,16 @@
const utils = require("../lib/utils")
;(function () {
if (context.request().query("clear")) {
utils.clearSSRCache()
throw {
status: 200,
message: "cache cleared",
}
}
throw {
status: 500,
message: "ssr is only a dummy collection",
}
})()

BIN
api/img/pic.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View File

@@ -0,0 +1,9 @@
function requestTemperature() {
let response = context.http.fetch(
"https://api.openweathermap.org/data/2.5/weather?lat=50.98&lon=11.03&appid=21fa3d5930956682000f7a2db5f357c0"
)
let data = response.body.json()
let temperatureEntries = context.db.find("temperature")
context.db.update("temperature", temperatureEntries[0].id, { temperature: data.main.temp - 273.15 })
}
requestTemperature()

View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<title>{{formTitle}}</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
color: #333;
margin: 0;
padding: 20px;
}
.container {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
max-width: 500px;
margin: 0 auto;
}
h2,
h3 {
color: #444;
}
p {
margin-bottom: 10px;
line-height: 1.6;
}
.row-content > p {
margin-right: 10px;
display: inline-block;
}
</style>
</head>
<body>
<div class="container">
<h2>Hallo Wasserskianlage Erfurt,</h2>
<p>Sie haben eine neue {{formTitle}} erhalten! Hier sind die Details der Anfrage:</p>
{% for row in formularRows %}
<h3>{{row.0}}:</h3>
<div class="row-content">
{% for field in row.1 %} {% if field.0 == "" || field.1.2 %}
<p>{{field.1.0}}</p>
{% else %} {% if field.1.1 %}<br />
<p class="">{{field.1.0}}</p>
{% else %}
<p><strong>{{field.0}}:</strong> {{field.1.0}}</p>
{% endif %} {% endif %} {% endfor %}
</div>
{% endfor %}
<p>
Bitte nehmen Sie Kontakt mit dem Interessenten auf, um die {{formTitle}} zu besprechen und weitere
Schritte zu planen.
</p>
<p>Mit freundlichen Grüßen,<br />Ihre Webmakers!</p>
</div>
</body>
</html>

1
api/templates/spa.html Symbolic link
View File

@@ -0,0 +1 @@
../../frontend/spa.html