backend & types

This commit is contained in:
2024-01-27 18:58:35 +00:00
parent 91bfa0864d
commit 0b4a474180
219 changed files with 5211 additions and 12325 deletions

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { location } from "../store"
import { location } from "./lib/store"
export let url = ""
@@ -16,7 +16,7 @@
}
}
if (typeof window !== "undefined") console.log("App initialized")
if (typeof window !== "undefined") console.log("App initialized")
</script>
<h1>Hello World</h1>

12
frontend/src/api.ts Normal file
View File

@@ -0,0 +1,12 @@
import { apiRequest } from "../../api/hooks/lib/ssr"
export const api = async <T>(
endpoint: string,
options?: ApiOptions,
body?: any
): Promise<{ data: T; count: number } | any> => {
let data = await apiRequest(endpoint, options, body)
// @ts-ignore
console.log(data, "data")
return data
}

View File

@@ -3,7 +3,6 @@ import configClient from "../../api/hooks/config-client"
export const apiBaseURL = "/api/"
export const release = configClient.release
console.log("Release: ", release)
export const sentryDSN = "https://5063f9b5564d0fdece4e47a8e2e63672@sentry.basehosts.de/3"
export const sentryTracingOrigins = ["localhost", "project-domain.tld", /^\//]

View File

@@ -1,5 +1,5 @@
import App from "./components/App.svelte"
import { location } from "./store"
import App from "./App.svelte"
import { location } from "./lib/store"
const publishLocation = (_p?: string) => {
let _s: string
@@ -17,11 +17,8 @@ const publishLocation = (_p?: string) => {
}
const newLocation = {
path:
_p || (typeof window !== "undefined" && window.location?.pathname),
search: _p
? _s
: typeof window !== "undefined" && window.location?.search,
path: _p || (typeof window !== "undefined" && window.location?.pathname),
search: _p ? _s : typeof window !== "undefined" && window.location?.search,
hash: _p ? _h : typeof window !== "undefined" && window.location?.hash,
push: !!_p,
pop: !_p,
@@ -35,9 +32,7 @@ if (typeof history !== "undefined") {
if (typeof Proxy !== "undefined") {
// modern browser
const historyApply = (target, thisArg, argumentsList) => {
publishLocation(
argumentsList && argumentsList.length >= 2 && argumentsList[2]
)
publishLocation(argumentsList && argumentsList.length >= 2 && argumentsList[2])
Reflect.apply(target, thisArg, argumentsList)
}
@@ -57,11 +52,7 @@ if (typeof history !== "undefined") {
publishLocation(url)
return pushStateFn.apply(history, arguments)
}
history.replaceState = function (
data: any,
title: string,
url?: string
) {
history.replaceState = function (data: any, title: string, url?: string) {
publishLocation(url)
return replaceStateFn.apply(history, arguments)
}
@@ -83,4 +74,4 @@ const app = new App({
hydrate,
})
export default app
export default app

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import type { Writable } from "svelte/store"
export let label = "" // Der Text, der neben der Checkbox angezeigt wird
export let bindValue: any // Der Wert der Checkbox
export let formValues: Writable<FormValues> // Das formValues-Objekt aus dem übergeordneten Komponenten
export let id = "" // Eindeutige ID für die Checkbox, um den Label korrekt zuzuweisen
export let name = "" // Name des Formularelements
</script>
<label class="checkbox-container" for="{id}">
<input type="checkbox" bind:checked="{bindValue}" {id} {name} bind:this="{formValues[name]}" />
<span>{label}</span>
</label>
<style>
/* Sie können hier den Stil für Ihre Checkbox anpassen */
</style>

View File

@@ -0,0 +1,48 @@
<script lang="ts">
import type { Writable } from "svelte/store"
export let groupTitle: string
export let checkboxes: { name: string; emailName: string }[]
export let formValues: Writable<FormValues>
export let rowNr: number
</script>
<div class="checkbox-group">
<h3>{groupTitle}</h3>
<div class="containerr">
{#each checkboxes as checkbox, i}
<label class="checkbox-label">
<input
type="checkbox"
class="checkbox-input checkit"
bind:this="{$formValues[`checkbox_${groupTitle}_${rowNr}_${checkbox.emailName}`]}"
/>
<span class="checkit-span"></span>
{checkbox.name}
</label>
{/each}
</div>
</div>
<style lang="less">
.checkbox-group {
.checkbox-input {
vertical-align: middle;
margin-right: 8px;
width: fit-content !important;
}
.checkbox-label {
width: 100%;
vertical-align: middle;
align-items: center;
display: flex;
gap: 10px;
}
& > .containerr {
margin-top: 10px;
display: flex;
flex-direction: column;
gap: 10px;
}
}
</style>

View File

@@ -0,0 +1,133 @@
<script lang="ts">
import { CalendarView } from "fluent-svelte"
import type { Writable } from "svelte/store"
export let groupTitle: string
export let datePickerProps: DatePickerProps
export let formValues: Writable<FormValues>
export let rowNr: number
export let formCol: FormColumn
const today = new Date()
const oneYearFromNow = new Date(today)
oneYearFromNow.setFullYear(today.getFullYear() + 1)
const indexToDay: Record<number, string> = {
0: "sunday",
1: "monday",
2: "tuesday",
3: "wednesday",
4: "thursday",
5: "friday",
6: "saturday",
}
function generateBlackoutDates(props: DatePickerProps): Date[] {
let blackoutDates = []
// Funktion, um einen ISO-String in ein vereinfachtes Date-Objekt umzuwandeln
const dateFromISOString = (isoString: string) => {
const date = new Date(isoString)
return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
}
const dayToIndex: Record<string, number> = {
sunday: 0,
monday: 1,
tuesday: 2,
wednesday: 3,
thursday: 4,
friday: 5,
saturday: 6,
}
// Initialisiere ein aktuelles Datum auf heute
let currentDate = new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate()))
// Schleife durch die nächsten 365 Tage
for (let i = 0; i <= 365; i++) {
// Überprüfen, ob dieses Datum erlaubt ist
const isAllowed = props.allowedDateRanges.some((range) => {
const fromDate = dateFromISOString(range.from)
const toDate = dateFromISOString(range.to)
return currentDate >= fromDate && currentDate <= toDate
})
// Überprüfen, ob der Wochentag ausgeschlossen ist
const currentDayIndex = currentDate.getUTCDay()
const currentDayString = indexToDay[currentDayIndex]
const isExcluded = props.excludeDays?.includes(currentDayString)
// Wenn nicht erlaubt oder ausgeschlossen, zu den Blackout-Daten hinzufügen
if (!isAllowed || isExcluded) {
blackoutDates.push(new Date(currentDate))
}
// Zum nächsten Tag wechseln
currentDate.setUTCDate(currentDate.getUTCDate() + 1)
}
return blackoutDates
}
let value = new Date()
const blackoutDates = generateBlackoutDates(datePickerProps)
function setFormValues() {
$formValues[`datepicker_${groupTitle}_${rowNr}_${formCol.datePickerEmailTitle}`] = {
value: value.toLocaleDateString("de-DE"),
required: !formCol.datePickerNotRequired,
}
}
//needs email conntection
$: {
if (value) {
setFormValues()
}
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="container">
<div
class="datepicker"
on:click|preventDefault|stopPropagation
on:keydown|stopPropagation
on:submit|stopPropagation|preventDefault
>
<CalendarView
weekStart="{1}"
bind:vaue="{value}"
on:change="{(e) => {
value = e.detail
}}"
min="{today}"
max="{oneYearFromNow}"
blackout="{blackoutDates}"
locale="de-DE"
/>
</div>
</div>
<style lang="less">
@import url("https://unpkg.com/fluent-svelte/theme.css");
.container {
width: 100%;
display: flex;
justify-content: flex-start;
--fds-solid-background-quarternary: var(--background-color);
--fds-text-primary: var(--normal-font-color);
--fds-text-secondary: var(--normal-font-color-80);
--fds-text-tertiary: var(--normal-font-color-50);
--fds-text-disabled: var(--normal-font-color-30);
--fds-accent-disabled: var(--opposite-bg-color-5);
--fds-control-strong-stroke-default: var(--opposite-bg-color-80);
.datepicker {
width: fit-content;
margin: 5px 0px;
box-shadow: 0 0 25px 10px var(--opposite-bg-color-5);
}
}
</style>

View File

@@ -0,0 +1,200 @@
<script lang="ts">
import CheckboxGroup from "./CheckboxGroup.svelte"
import Datepicker from "./Datepicker.svelte"
import FormLabelNumberBlock from "./FormLabelNumberBlock.svelte"
import type { Writable } from "svelte/store"
import Select from "./Select.svelte"
export let formRow: FormRow
export let index: number
export let formValues: Writable<FormValues>
/* function getSortedFields(column: FormColumn) {
const fields = [
...(column.text.length ? [{ text: column.text, type: "text", order: column.textfieldOrder ?? 3 }] : []),
...(column.showTimes ? [{ type: "times", order: column.timesfieldOrder ?? 3, times: column.times }] : []),
...(column.showDate
? [{ type: "date", order: column.datefieldOrder ?? 3, datePlaceholder: column.datePlaceholder }]
: []),
]
return fields.sort((a, b) => a.order - b.order)
}*/
//formRow.columns = formRow.columns.map((e) => getSortedFields(e))
function removeInvalid(e: Event) {
let element = e.currentTarget as HTMLElement
if (element) element.classList.remove("invalid")
}
</script>
<div class="form-row">
<h3 style="margin-bottom: -0.5rem;">
{#if formRow.showRowName}{formRow.rowName || ""}{/if}
</h3>
<div class="form-cols">
{#each formRow.columns as column, columnIndex}
<div class="form-column">
<h3>{column?.title ?? ""}</h3>
{#if column?.annotation}
<p>{column?.annotation}</p>
{/if}
{#if column.showLabelNumber}
<FormLabelNumberBlock {column} {formValues} rowIndex="{index}" />
{/if}
{#if column?.showTimes}
<label bind:this="{$formValues[`times_${column?.title ? column.title + '_' : ''}label`]}">
<select
id="time-select"
bind:this="{$formValues[
`times_${column?.title ?? 'Zeiten'}_${column.timesfieldOrder}_${index}_${
column.emailNameTimes || columnIndex + 'invalidtimes'
}`
]}"
required="{column?.timeNotRequired !== true}"
on:change="{removeInvalid}"
>
<option value="" disabled selected>Bitte Uhrzeit wählen</option>
{#each column?.times ?? [] as time}
<option value="{time?.timeFrom}-{time?.timeTo}">
{time?.timeFrom} - {time?.timeTo}
</option>
{/each}
</select>
</label>
{/if}
{#if column?.showSelect}
<label bind:this="{$formValues[`select_${column?.title ? column.title + '_' : ''}label`]}">
<select
required="{column?.dateSelectNotRequired !== true}"
bind:this="{$formValues[
`select_${column?.title ?? 'Auswahl'}_${column.datefieldOrder}_${index}_${
column?.emailNameTime || columnIndex + 'invalidtime'
}`
]}"
on:change="{removeInvalid}"
>
<option value="" disabled selected>{column.selectTitle}</option>
{#each column?.selectEntries as entry}
<option value="{entry?.leftSide}-{entry?.rightSide}">
{entry?.leftSide}-{entry?.rightSide}
</option>
{/each}
</select>
</label>
{/if}
{#if column.showDate}
<label bind:this="{$formValues[`date_${column?.title ? column.title + '_' : ''}label`]}">
<input
class="date"
type="date"
required="{column?.dateNotRequired !== true}"
on:change="{removeInvalid}"
bind:this="{$formValues[
`date_${column?.title ?? 'Datum'}_${column.datefieldOrder}_${index}_${
column?.emailNameDate || columnIndex + 'invaliddate'
}`
]}"
/>
</label>
{/if}
{#if column.showNumber}
<label
bind:this="{$formValues[
`input_${column.title ? column.title + '_' : ''}${column?.numberPlaceholder}_label`
]}"
>
<input
type="number"
step="any"
required="{column?.numberNotRequired !== true}"
on:change="{removeInvalid}"
placeholder="{column?.numberPlaceholder}"
bind:this="{$formValues[
`input_${column.title ? column.title + '_' : ''}${column?.numberPlaceholder}_${
column.numberfieldOrder
}_${index}_${column?.emailNameNumber || columnIndex + 'invalidnumber'}`
]}"
/>
</label>
{/if}
{#if column.showCheckboxGroup}
<CheckboxGroup
checkboxes="{column.checkboxes}"
groupTitle="{column.groupTitle}"
{formValues}
rowNr="{index}"
/>
{/if}
{#if column.showDatePicker}
<Datepicker
datePickerProps="{column.datePickerProps}"
groupTitle="{column.groupTitle}"
{formValues}
rowNr="{index}"
formCol="{column}"
/>
{/if}
{#if column.showMultiSelect}
<Select
groupTitle="{column.groupTitle}"
{formValues}
rowNr="{index}"
formCol="{column}"
options="{column.multiSelectOptions}"
/>
{/if}
{#each column.text ?? [] as textField, textFieldIndex}
<div>
<h3 class="textTitle">{textField.textTitle || ""}</h3>
{#if textField?.textArea}
<label bind:this="{$formValues[`textarea_Nachricht_label`]}">
<textarea
placeholder="{textField?.textPlaceholder}"
required="{textField?.notRequired !== true}"
on:change="{removeInvalid}"
bind:this="{$formValues[
`textarea_Nachricht_${textField.textfieldOrder}_${index}_${
textField.emailName || columnIndex + 'invalidtext' + textFieldIndex
}`
]}"
></textarea>
</label>
{:else}
<label
bind:this="{$formValues[
`input_${column.title ? column.title + '_' : ''}${textField?.textPlaceholder}_label`
]}"
>
<input
type="text"
placeholder="{textField?.textPlaceholder}"
on:change="{removeInvalid}"
required="{textField?.notRequired !== true}"
bind:this="{$formValues[
`${
textField?.telValidation
? 'Telefon'
: textField?.emailValidation
? 'Email'
: 'input'
}_${column.title ? column.title + '_' : ''}${textField?.textPlaceholder}_${
textField.textfieldOrder
}_${index}_${
textField.emailName || columnIndex + 'invalidtext' + textFieldIndex
}`
]}"
/>
</label>
{/if}
</div>
{/each}
</div>
{/each}
</div>
</div>

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import type { Writable } from "svelte/store"
export let column: FormColumn
export let formValues: Writable<FormValues>
export let rowIndex: number
let blockContainer: HTMLDivElement
$formValues["blockGroups"] = new Set(column.labelNumber.map((e) => e.group))
</script>
<div class="blockContainer" bind:this="{blockContainer}">
{#each column.labelNumber as outerblock, i}
<div class="{`block`}" bind:this="{$formValues['blockGroups'][i]}">
<h3>{outerblock.title}</h3>
<div class="innterBlockContainer">
{#each outerblock.block as innerBlock}
<div class="innerBlock">
<div class="label">{innerBlock.label}</div>
<input
placeholder="0"
on:change="{(e) => {
let element = e.currentTarget
element.classList.remove('border-red')
blockContainer.classList.remove('invalidBlocks')
}}"
type="number"
name="{outerblock.group}"
class="{`group-${outerblock.group}`}"
bind:this="{$formValues[
`numberLabel_${outerblock.title ?? ''}_${innerBlock.label}_${rowIndex}_${
innerBlock.emailName
}`
]}"
/>
</div>
{/each}
</div>
</div>
{/each}
</div>
<style lang="less">
.block {
width: 100%;
padding: 25px 0px;
h3 {
margin-bottom: 0px;
}
.innerBlock {
width: 100%;
display: flex;
justify-content: space-between;
.label {
display: flex;
align-items: flex-end;
}
input {
text-align: center;
width: 80px;
font-size: 1rem;
}
}
}
</style>

View File

@@ -0,0 +1,401 @@
<script lang="ts">
import Form from "./Form.svelte"
import MobileForm from "./MobileForm.svelte"
import { writable } from "svelte/store"
import { validateFields } from "../../../functions/validateFields"
import { sendForm } from "../../../functions/sendForm"
import { navigate } from "svelte-routing"
import { onMount } from "svelte"
export let col: Column
export let siteId: string
export let rowNr: number
export let row: Row
export let rows: Row[]
let formSend = false
let formValues = writable<FormValues>({})
function submitForm() {
const values: Array<ValueEntry> = Object.entries($formValues).map((entry: ObjectEntry) => {
const key: string = entry[0]
const value: CustomHTMLElement = entry[1]
if (!key.includes("numberLabel")) {
return [key, [value.checked || value.value, value.required]] as ValueEntry
} else {
return [key, [value.value, value, value.getAttribute("name"), value.required]] as ValueEntry
}
})
const fields: Array<ValueEntry> = values.filter((entry: ValueEntry) => !entry[0].includes("label"))
const validation = validateFields(fields)
if (validation.length) {
validation.forEach((error) => {
// @ts-ignore
if (error[0].includes("block")) {
// @ts-ignore
error[1]()
} else {
// @ts-ignore
$formValues[error[1]].classList.add("invalid")
const label = $formValues[`${error[1]}_label`]
const errorElement = document.createElement("div")
errorElement.className = "error-message"
errorElement.textContent = error[0] as string
label?.appendChild(errorElement)
}
})
} else {
// @ts-ignore
const formObj: FormObj = {}
fields.forEach((entry) => {
if (Array.isArray(entry[1]) && entry[1].length == 4) {
// @ts-ignore
if (entry[1][0]) formObj[entry[0]] = entry[1]
} else {
if (!entry[1][0] && !entry[1][1]) return
// @ts-ignore
formObj[entry[0]] = entry[1][0]
}
})
let form: any
row.column.forEach((col) => {
if (col.contentType == "form") form = col
})
if (!form) return
formObj["formRows"] = form.formRows.map((r: FormRow) => r.rowName)
formObj["formTitle"] = form.formEmailTitle
formSend = true
const hny = document.getElementById("hny") as HTMLInputElement
if (hny) formObj["honey"] = hny.checked
sendForm(formObj)
}
}
let innerWidth = typeof window !== "undefined" ? window?.innerWidth || 0 : 0
if (typeof window !== "undefined") {
onMount(() => {
const handleResize = () => {
innerWidth = window.innerWidth
}
window.addEventListener("resize", handleResize)
// Cleanup function
return () => {
window.removeEventListener("resize", handleResize)
}
})
}
</script>
{#if formSend}
<div class="success-message">
<h1>Formular erfolgreich gesendet!</h1>
<p>Vielen Dank für Ihre Anfrage. Wir werden uns in Kürze bei Ihnen melden.</p>
</div>
{:else}
<form
class="form-rows"
on:submit="{(e) => {
e.preventDefault()
submitForm()
}}"
>
{#each col.formRows ?? [] as formRow, i}
{#if innerWidth < 768}
<MobileForm {formRow} {formValues} index="{i}" />
{:else}
<Form {formRow} {formValues} index="{i}" />
{/if}
{/each}
<div class="row additional" style="padding: 0px;">
<div class="data-protection">
<label bind:this="{$formValues[`agreement_label`]}">
<input
required="{true}"
class="checkit"
type="checkbox"
on:change="{(e) => {
let element = e.currentTarget
element.classList.remove('invalid')
}}"
bind:this="{$formValues['agreement']}"
/>
<span class="checkit-span"></span>
</label>
<div class="datasec">
<button on:click|preventDefault="{() => navigate('/datenschutz')}" class="link">
Datenschutz
</button>
akzeptieren
</div>
</div>
<input
type="checkbox"
name="contact_me_by_fax_only"
id="hny"
value="1"
style="display:none !important"
tabindex="-1"
autocomplete="off"
/>
<button class="submit-request" type="submit">Anfrage absenden</button>
</div>
</form>
{/if}
<style lang="less" global>
@import "../../../assets/css/variables.less";
input,
select,
textarea,
.data-protection {
margin: 5px 0px;
box-shadow: 0 0 25px 10px var(--opposite-bg-color-5);
}
.success-message {
h1 {
color: var(--heading-font-color);
font-size: 36px;
margin-top: 50px;
}
p {
font-size: 20px;
margin-top: 20px;
}
}
.invalidBlocks {
border: 2px solid rgb(255, 0, 0) !important;
position: relative;
&::after {
font-size: 0.9rem !important;
color: red !important;
position: absolute;
bottom: 2px;
content: "Bitte wähle entweder eine Kartenanzahl oder einen Wunschbetrag aus.";
}
}
.border-red {
border-color: red !important;
}
.no-margin {
margin-top: 15px !important;
}
.invalid {
border: 2px solid red !important;
}
.error-message {
font-size: 0.9rem !important;
color: red !important;
position: absolute;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
height: 100%;
}
input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}
.checkit {
display: none;
}
.checkit-span {
height: 20px;
width: 20px;
border: 1px solid grey;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
[type="checkbox"]:checked + span:before {
content: "\2714";
position: absolute;
transform-origin: bottom;
}
.form-rows {
display: flex;
flex-direction: column;
gap: 1.5rem;
width: 100%;
h3 {
font-weight: bold !important;
margin-bottom: 0px !important;
}
.form-row {
display: flex;
flex-direction: column;
width: 100%;
gap: 1.5rem;
.date {
max-width: 100%;
box-sizing: border-box;
}
label {
width: 100% !important;
font-size: inherit;
input,
select,
textarea {
width: 100%;
font-size: inherit;
}
}
.form-cols {
display: flex;
gap: 1.5rem;
width: 100%;
}
}
.form-column {
display: flex;
flex: 1;
flex-direction: column;
justify-content: flex-start;
width: 100%;
gap: 1rem;
border-radius: 4px;
}
h3 {
font-size: 1.2rem;
font-weight: bold;
}
p {
margin-bottom: 0.5rem;
color: var(--hover-color);
}
input,
select,
textarea,
.data-protection {
padding: 10px 20px;
border: 0px solid var(--opposite-bg-color);
border-bottom: 3px solid var(--heading-font-color);
outline: 0px solid var(--opposite-bg-color);
color: var(--opposite-bg-color);
background-color: var(--background-color);
resize: none;
}
.data-protection {
display: flex;
flex-wrap: nowrap;
gap: 5px !important;
justify-content: start;
flex-direction: row !important;
}
input[type="date"] {
font-family: inherit;
width: 100% !important;
position: relative;
}
select {
padding: 10px 20px;
border: 0;
border-bottom: 3px solid var(--heading-font-color);
outline: none;
}
select:focus {
border-bottom-color: var(--heading-font-color);
}
#time-select {
appearance: none;
background-image: url("../../../../../media/clock.svg");
background-repeat: no-repeat;
background-position: right 20px center;
background-size: 18px;
option {
padding: 10px 20px;
background-color: var(--background-color);
}
}
@media @mobile {
.date {
width: 100vw !important;
}
}
@media @tablet {
.date {
width: 100% !important;
}
.form-cols {
flex-direction: row;
}
}
}
i .max-width {
max-width: @body-maxwidth !important;
}
.datasec {
display: flex;
align-items: center !important;
.link {
height: 100%;
display: flex;
align-items: flex-end !important;
text-decoration: underline;
margin-right: 3px;
color: rgb(14, 91, 146);
}
}
.additional {
display: flex;
@media @mobile {
flex-direction: column !important;
}
@media @tablet {
flex-direction: row !important;
}
gap: 2.5rem;
div {
flex: 1;
display: flex;
align-items: center;
justify-content: start;
}
.submit-request {
flex: 1;
box-shadow: 0 0 25px 10px var(--opposite-bg-color-5);
width: 100%;
display: flex;
align-items: center;
justify-content: start;
background-color: var(--heading-font-color);
color: var(--background-color);
padding: 10px 20px;
}
}
</style>

View File

@@ -0,0 +1,351 @@
<script lang="ts">
import FormLabelNumberBlock from "./FormLabelNumberBlock.svelte"
import type { Writable } from "svelte/store"
import CheckboxGroup from "./CheckboxGroup.svelte"
import Datepicker from "./Datepicker.svelte"
import Select from "./Select.svelte"
export let formRow: FormRow
export let formValues: Writable<FormValues>
export let index: number
function removeInvalid(e: Event) {
let element = e.currentTarget as HTMLElement
if (element) element.classList.remove("invalid")
}
function getPosition(column: FormColumn, pos: number, i = 0) {
let position = 0
if (pos == 0) return
if (column.showLabelNumber) position++
if (pos == 1) return position
if (column.showTimes) position++
if (pos == 2) return position
if (column.showSelect) position++
if (pos == 3) return position
if (column.showDate) position++
if (pos == 4) return position
if (column.showNumber) position++
return position + i
}
</script>
<div class="form-row">
<h3 style="margin-bottom: -0.5rem;">
{#if formRow.showRowName}{formRow.rowName || ""}{/if}
</h3>
<div class="form-cols mobile-fields">
{#each formRow.columns as column, columnIndex}
<h3>{column?.title ?? ""}</h3>
{#if column?.annotation}
<p>{column?.annotation}</p>
{/if}
{#if column.showLabelNumber}
<div class="{`column-${columnIndex} position-${getPosition(column, 0)}`}">
<FormLabelNumberBlock {column} {formValues} rowIndex="{index}" />
</div>
{/if}
{#if column?.showTimes}
<div class="column-{columnIndex} position-{getPosition(column, 1)}">
<label bind:this="{$formValues[`times_${column?.title ? column.title + '_' : ''}label`]}">
<select
id="time-select"
bind:this="{$formValues[
`times_${column?.title ?? 'Zeiten'}_${column.timesfieldOrder}_${index}_${
column.emailNameTimes || columnIndex + 'invalidtimes'
}`
]}"
required="{column?.timeNotRequired !== true}"
on:change="{removeInvalid}"
>
<option value="" disabled selected>Bitte Uhrzeit wählen</option>
{#each column?.times ?? [] as time}
<option value="{time?.timeFrom}-{time?.timeTo}">
{time?.timeFrom} - {time?.timeTo}
</option>
{/each}
</select>
</label>
</div>
{/if}
{#if column?.showSelect}
<div class="column-{columnIndex} position-{getPosition(column, 2)}">
<label bind:this="{$formValues[`select_${column?.title ? column.title + '_' : ''}label`]}">
<select
required="{column?.dateNotRequired !== true}"
bind:this="{$formValues[
`select_${column?.title ?? 'Auswahl'}_${column.datefieldOrder}_${index}_${
column?.emailNameTime || columnIndex + 'invalidtime'
}`
]}"
on:change="{removeInvalid}"
>
<option value="" disabled selected>{column.selectTitle}</option>
{#each column?.selectEntries as entry}
<option value="{entry?.leftSide}-{entry?.rightSide}">
{entry?.leftSide}-{entry?.rightSide}
</option>
{/each}
</select>
</label>
</div>
{/if}
{#if column.showDate}
<div class="column-{columnIndex} position-{getPosition(column, 3)}">
<label bind:this="{$formValues[`date_${column?.title ? column.title + '_' : ''}label`]}">
<input
class="date"
type="date"
required="{column?.dateNotRequired !== true}"
on:change="{removeInvalid}"
bind:this="{$formValues[
`date_${column?.title ?? 'Datum'}_${column.datefieldOrder}_${index}_${
column?.emailNameDate || columnIndex + 'invaliddate'
}`
]}"
/>
</label>
</div>
{/if}
{#if column.showNumber}
<div class=" column-{columnIndex} position-{getPosition(column, 4)}">
<label
bind:this="{$formValues[
`input_${column.title ? column.title + '_' : ''}${column?.numberPlaceholder}_label`
]}"
>
<input
type="number"
step="any"
required="{column?.numberNotRequired !== true}"
on:change="{removeInvalid}"
placeholder="{column?.numberPlaceholder}"
bind:this="{$formValues[
`input_${column.title ? column.title + '_' : ''}${column?.numberPlaceholder}_${
column.numberfieldOrder
}_${index}_${column?.emailNameNumber || columnIndex + 'invalidnumber'}`
]}"
/>
</label>
</div>
{/if}
{#if column.showCheckboxGroup}
<CheckboxGroup
checkboxes="{column.checkboxes}"
groupTitle="{column.groupTitle}"
{formValues}
rowNr="{index}"
/>
{/if}
{#if column.showDatePicker}
<Datepicker
datePickerProps="{column.datePickerProps}"
groupTitle="{column.groupTitle}"
{formValues}
rowNr="{index}"
formCol="{column}"
/>
{/if}
{#if column.showMultiSelect}
<Select
groupTitle="{column.groupTitle}"
{formValues}
rowNr="{index}"
formCol="{column}"
options="{column.multiSelectOptions}"
/>
{/if}
{#each column.text ?? [] as textField, textFieldIndex}
<div class="column-{columnIndex} position-{getPosition(column, 5 + textFieldIndex, textFieldIndex)}">
<h3 class="textTitle">{textField.textTitle || ""}</h3>
<div
class="column-{columnIndex} position-{getPosition(column, 5 + textFieldIndex, textFieldIndex)}"
>
{#if textField?.textArea}
<label bind:this="{$formValues[`textarea_Nachricht_label`]}">
<textarea
placeholder="{textField?.textPlaceholder}"
required="{textField?.notRequired !== true}"
on:change="{removeInvalid}"
bind:this="{$formValues[
`textarea_Nachricht_${textField.textfieldOrder}_${index}_${
textField.emailName || columnIndex + 'invalidtext' + textFieldIndex
}`
]}"
></textarea>
</label>
{:else}
<label
bind:this="{$formValues[
`input_${column.title ? column.title + '_' : ''}${textField?.textPlaceholder}_label`
]}"
>
<input
type="text"
placeholder="{textField?.textPlaceholder}"
on:change="{removeInvalid}"
required="{textField?.notRequired !== true}"
bind:this="{$formValues[
`${
textField?.telValidation
? 'Telefon'
: textField?.emailValidation
? 'Email'
: 'input'
}_${column.title ? column.title + '_' : ''}${textField?.textPlaceholder}_${
textField.textfieldOrder
}_${index}_${
textField.emailName || columnIndex + 'invalidtext' + textFieldIndex
}`
]}"
/>
</label>
{/if}
</div>
</div>
{/each}
{/each}
</div>
</div>
<style lang="less">
.mobile-fields {
display: flex;
flex-direction: column;
}
.form-field {
order: 1;
}
/* Order for first column */
.column-0.position-0 {
order: 1;
}
.column-0.position-1 {
order: 5;
}
.column-0.position-2 {
order: 9;
}
.column-0.position-3 {
order: 13;
}
.column-0.position-4 {
order: 17;
}
.column-0.position-5 {
order: 21;
}
.column-0.position-6 {
order: 25;
}
.column-0.position-7 {
order: 29;
}
.column-0.position-8 {
order: 33;
}
.column-0.position-9 {
order: 37;
}
/* Order for second column */
.column-1.position-0 {
order: 2;
}
.column-1.position-1 {
order: 6;
}
.column-1.position-2 {
order: 10;
}
.column-1.position-3 {
order: 14;
}
.column-1.position-4 {
order: 18;
}
.column-1.position-5 {
order: 22;
}
.column-1.position-6 {
order: 26;
}
.column-1.position-7 {
order: 30;
}
.column-1.position-8 {
order: 34;
}
.column-1.position-9 {
order: 38;
}
/* Order for third column */
.column-2.position-0 {
order: 3;
}
.column-2.position-1 {
order: 7;
}
.column-2.position-2 {
order: 11;
}
.column-2.position-3 {
order: 15;
}
.column-2.position-4 {
order: 19;
}
.column-2.position-5 {
order: 23;
}
.column-2.position-6 {
order: 27;
}
.column-2.position-7 {
order: 31;
}
.column-2.position-8 {
order: 35;
}
.column-2.position-9 {
order: 39;
}
/* Order for fourth column */
.column-3.position-0 {
order: 4;
}
.column-3.position-1 {
order: 8;
}
.column-3.position-2 {
order: 12;
}
.column-3.position-3 {
order: 16;
}
.column-3.position-4 {
order: 20;
}
.column-3.position-5 {
order: 24;
}
.column-3.position-6 {
order: 28;
}
.column-3.position-7 {
order: 32;
}
.column-3.position-8 {
order: 36;
}
.column-3.position-9 {
order: 40;
}
</style>

View File

@@ -0,0 +1,84 @@
<script lang="ts">
import Select from "svelte-select/Select.svelte"
import { onMount } from "svelte"
import type { Writable } from "svelte/store"
export let options: MultiSelectOptions[] = []
export let groupTitle: string
export let formValues: Writable<FormValues>
export let rowNr: number
export let formCol: FormColumn
let valueStorage: any[] = []
let value: string[] = []
let values = options.map((option) => ({ label: option.name, value: option.name }))
let lastCustomInput: string | null = null
function setFormValues() {
// @ts-ignore
$formValues[`selectMultiple_${groupTitle}_${rowNr}_${formCol.multiSelectEmailTitle}`] = {
value: value.toString(),
required: !formCol.multiSelectNotRequired,
}
}
function handleInputChange(e: Event) {
const newCustomInput = (e.target as HTMLInputElement).value
// Remove the last custom input if it exists
if (lastCustomInput) {
values = values.filter((item) => item.label !== lastCustomInput)
}
// Add the new custom input
if (newCustomInput && !values.some((item) => item.label === newCustomInput)) {
values = [...values, { label: newCustomInput, value: newCustomInput }]
}
// Update lastCustomInput
lastCustomInput = newCustomInput
}
$: {
if (value.length) {
setFormValues()
}
}
onMount(() => {
if (typeof window !== "undefined") {
const inputEl = document.querySelector(".svelte-select input")
if (inputEl) {
inputEl.addEventListener("input", handleInputChange)
}
return () => {
if (inputEl) {
inputEl.removeEventListener("input", handleInputChange)
}
}
}
})
$: if (!valueStorage) {
valueStorage = []
}
$: value = valueStorage.map((item) => item.value)
</script>
<Select
bind:items="{values}"
inputAttributes="{{ autocomplete: 'on' }}"
placeholder=""
showChevron="{true}"
clearable="{true}"
hideEmptyState="{true}"
searchable="{true}"
multiple="{true}"
bind:value="{valueStorage}"
on:clear="{() => (valueStorage = [])}"
/>
<style lang="less" global>
@import "../../../assets/css/variables.less";
@import "../../../assets/css/svelte-select.less";
</style>

View File

@@ -0,0 +1,7 @@
import { api } from "../../../../../api"
export async function sendForm(body: any) {
let response: any
response = await api("forms", { method: "POST" }, { formular: body })
return response
}

View File

@@ -0,0 +1,68 @@
export function validateFields(fieldsArray: ValueEntry[]): (string | (() => void))[][] {
const errors = []
let selectedGroup: number
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")
if (blockContainer.length == 0) return
blockContainer[0].classList.add("invalidBlocks")
}
const validateNumber = (value: string, field: any, element: HTMLElement) => {
if (!numberRegex.test(`${value}`)) errors.push(["block", () => element.classList.add("border-red")])
}
fieldsArray.forEach(([field, value]) => {
if (field === "blockGroups" || (typeof field === "string" && field.includes("numberLabel"))) {
if (!field.includes("numberLabel")) return
// @ts-ignore
let [elementValue, element, group, boolean] = value
element = element as HTMLElement
if (!elementValue) return
if (selectedGroup !== undefined) {
if (group !== String(selectedGroup)) errors.push(["block", wholeBlockInvalid])
else validateNumber(elementValue, field, element)
} else {
//@ts-ignore
selectedGroup = group
validateNumber(elementValue, field, element)
}
return
}
value = value
let required = value[1]
value = value[0]
if (!required && !value) return
if (!value) {
errors.push(["Eingabe ist erforderlich.", field])
} else if (typeof field === "string") {
if (field.includes("number_")) {
if (!numberRegex.test(`${value}`)) errors.push(["Ungültiger numerischer Wert.", field])
} else if (field.includes("agreement_") && typeof value == "boolean") {
if (value !== true) errors.push(["Bitte das Kontrollkästchen anklicken.", field])
} else if (field.includes("Email_") && typeof value == "string") {
if (!emailRegex.test(value)) errors.push(["Ungültiges E-Mail-Format.", field])
} else if (field.includes("date_") && typeof value == "string") {
if (!dateRegex.test(value)) errors.push(["Ungültiges Datumsformat.", field])
} else if (field.includes("times_") && typeof value == "string") {
if (!timeRegex.test(value) && typeof value == "string") errors.push(["Ungültiges Zeitformat.", field])
} else if (field.includes("Telefon_") && typeof value == "string") {
if (!phoneRegex.test(value)) errors.push(["Ungültiges Telefonnummernformat.", field])
}
}
})
const blockContainer = document.getElementsByClassName("blockContainer")
// @ts-ignore
if (blockContainer.length && selectedGroup === undefined) {
errors.push(["block", wholeBlockInvalid])
}
return errors
}

View File

@@ -0,0 +1,6 @@
import { api } from "../../../api"
export async function loadContent(): Promise<Content[]> {
let site = await api<Content[]>("content", {})
return site.data
}

View File

@@ -0,0 +1,7 @@
import { api } from "../../../api"
export async function loadNavigation(): Promise<Navigation[]> {
let nav = await api<Navigation[]>("navigation", {})
console.log(nav.data, "nav")
return nav.data
}

View File

@@ -1,3 +1,3 @@
import App from "./components/App.svelte"
import App from "./App.svelte"
export default App