diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..251cc86 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,21 @@ +{ + "name": "Node", + "image": "mcr.microsoft.com/devcontainers/typescript-node:0-18", + "features": { + "ghcr.io/devcontainers-contrib/features/curl-apt-get:1": {}, + "ghcr.io/devcontainers-contrib/features/neovim-apt-get:1": {} + }, + "remoteUser": "node", + "postCreateCommand": "cd /workspaces/ferienpass-anmeldung && npm install && npm install @google/clasp --global", + "customizations": { + "vscode": { + "extensions": [ + "editorconfig.editorconfig", + "xabikos.javascriptsnippets", + "ecmel.vscode-html-css", + "github.copilot", + "GitHub.vscode-github-actions" + ] + } + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..35ce3ee --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..f655c99 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,40 @@ +name: Deploy + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + name: Build GitHub Pages site + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Patch and copy artifacts + run: | + mkdir _site + cp ferienpass.webp ferienpass-transparent.webp ./_site/ + GOOGLE_APP_SCRIPS_ID=${{ secrets.GOOGLE_APP_SCRIPS_ID }} envsubst < index.html > ./_site/index.html + + - name: Upload artifacts + uses: actions/upload-pages-artifact@v1 + + deploy: + name: Deploy GitHub Pages site + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f70112 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.clasp.json +.clasprc.json +node_modules diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ad92582 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSave": true +} diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..7e1306a --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +booking.ferienpass-seeberg.ch diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ca890c4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Ferienpass Seeberg, Oliver Gut + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ef392d --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +[![Languages](https://skillicons.dev/icons?i=js,html,css,gcp,bash,githubactions,linux,vscode)](https://skillicons.dev) + +[![Deploy](https://github.com/flenny/ferienpass-seeberg/actions/workflows/deploy.yml/badge.svg)](https://github.com/flenny/ferienpass-seeberg/actions/workflows/deploy.yml) +![GitHub](https://img.shields.io/github/license/flenny/ferienpass-seeberg) + +# Ferienpass Seeberg Registration Form + +Welcome to the ferienpass-seeberg repository! This repository contains the source code and documentation for the registration form of the Ferienpass Seeberg, a holiday program for kids in the Seeberg region of Switzerland. + +## About the Ferienpass Seeberg + +The Ferienpass Seeberg is a popular program for children between the ages of 6 and 16, organized by the association Ferienpass Seeberg. The program offers a variety of activities and events during one week in the summer holidays, such as sports, crafts, excursions, and much more. Children can choose from a wide range of activities and have fun with other kids while learning new things. + +## About the Registration Form + +The registration form is primarily designed to allow parents to book courses for their children. The website is built using HTML, CSS, and JavaScript and uses Google Apps Script as a backend for processing the course bookings. The registration form includes the following features: + +- Information about the program, including dates, activities, and prices +- Detailed information about each course, including availability and age requirements +- A registration form for parents to book courses for their children +- An administration interface (Google Spreadsheet) to manage the courses, registrations and volunteers + +## Contributing + +We welcome contributions to the ferienpass-seeberg repository! If you find a bug or have a feature request, please open an issue on the repository. + +## License + +The ferienpass-seeberg repository is released under the MIT License. See the LICENSE file for details. + +## Contact + +If you have any questions or comments about the Ferienpass Seeberg or the website, please contact us at support@ferienpass-seeberg.ch. diff --git a/ferienpass-transparent.webp b/ferienpass-transparent.webp new file mode 100644 index 0000000..df7524a Binary files /dev/null and b/ferienpass-transparent.webp differ diff --git a/ferienpass.webp b/ferienpass.webp new file mode 100644 index 0000000..c197d40 Binary files /dev/null and b/ferienpass.webp differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..06b58cf --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ + + + + Ferienpass Seeberg - Anmeldung Ferienpass + + + + + + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6ca84c8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,17 @@ +{ + "name": "ferienpass-anmeldung", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@types/google-apps-script": "^1.0.59" + } + }, + "node_modules/@types/google-apps-script": { + "version": "1.0.59", + "resolved": "https://registry.npmjs.org/@types/google-apps-script/-/google-apps-script-1.0.59.tgz", + "integrity": "sha512-vOtsegVU/BrgofBsx6Ckzz4jDNp2zEwGHHc20AAouyP6YzytCF5vIBCSETBjsIHZya+nA8A/f7YaSkCe29jbvQ==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3e8d1b5 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@types/google-apps-script": "^1.0.59" + } +} diff --git a/src/Server.js b/src/Server.js new file mode 100644 index 0000000..a72951b --- /dev/null +++ b/src/Server.js @@ -0,0 +1,270 @@ +const BOOKINGS_SHEET_NAME = 'Bookings' +const EVENTS_SHEET_NAME = 'Events' +const VOLUNTEERS_SHEET_NAME = 'Volunteers' +const SCRIPT_LOCK_TIMEOUT = 40000 +const EVENT_CACHE_IDENTIFIER = 'event-cache' +const BOOKINGS_CACHE_IDENTIFIER = 'bookings-cache' +const CACHE_EXPIRATION_TIMEOUT = 360 +const TIMESTAMP_FORMAT = 'dd.MM.yyyy HH:mm:ss' +const TIME_ZONE = 'Europe/Zurich' +const EVENTS_SHEET_ID_COLUMN = 'A:A' +const EVENTS_SHEET_NAME_COLUMN = 'B:B' +const EVENTS_SHEET_COSTS_COLUMN = 'E:E' +const EVENTS_SHEET_ADDITIONAL_COSTS_COLUMN = 'F:F' +const EVENTS_SHEET_PARTICIPANT_LIMIT_COLUMN = 'M:M' +const BOOKINGS_SHEET_EVENT_ID_COLUMN = 'L' + +/** + * Sets up the script properties, which are used to store the spreadsheet id and the email addresses. + * This function needs to be executed only once, when the script is first installed. + * */ +function setupScriptProperties() { + PropertiesService.getScriptProperties().setProperties({ + SPREADSHEET_ID: SpreadsheetApp.getActiveSpreadsheet().getId(), + EMAIL_FROM_NAME: 'John Doe', + EMAIL_FROM_ADDRESS: 'john.doe@example.com', + EMAIL_SUPPORT_REQUESTS: 'support@example.com', + }, true) +} + +/** Gets the index.html page for every HTTP GET reguest. */ +function doGet(request) { + const template = HtmlService.createTemplateFromFile(request.parameter?.action ?? 'index') + template.data = { + reference: request.parameter?.reference, + bookingId: request.parameter?.bookingId, + origin: request.parameter?.origin ?? ScriptApp.getService().getUrl(), + } + return template.evaluate() + .addMetaTag('viewport', 'width=device-width, initial-scale=1') + .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) +} + +/** Gets the content for the specified @filename. */ +function include(filename) { return HtmlService.createHtmlOutputFromFile(filename).getContent() } + +/** Gets all the events including updated availability information. */ +function getEvents() { + const result = CacheService.getScriptCache().get(EVENT_CACHE_IDENTIFIER) ?? + updateCache(EVENT_CACHE_IDENTIFIER, EVENTS_SHEET_NAME) + return Object.values(JSON.parse(result)) +} + +/** Gets the bookings for the specified reference. */ +function getBookings(reference) { + const result = CacheService.getScriptCache().get(BOOKINGS_CACHE_IDENTIFIER) ?? + updateCache(BOOKINGS_CACHE_IDENTIFIER, BOOKINGS_SHEET_NAME) + return Object.values(JSON.parse(result)) + .filter(booking => booking?.find(entry => entry === reference)) +} + +/** Updates all caches (event- and booking cache). */ +function updateCaches() { + updateCache(EVENT_CACHE_IDENTIFIER, EVENTS_SHEET_NAME) + updateCache(BOOKINGS_CACHE_IDENTIFIER, BOOKINGS_SHEET_NAME) +} + +function processForm(formObject) { + const lock = LockService.getScriptLock() + if (!lock.tryLock(SCRIPT_LOCK_TIMEOUT)) throw new Error('Could not obtain lock after 40 seconds.') + + try { + const spreadsheet = SpreadsheetApp.openById( + PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID') + ) + + const bookingsSheet = spreadsheet.getSheetByName(BOOKINGS_SHEET_NAME) + const bookingsSheetHeaders = bookingsSheet.getRange(1, 1, 1, bookingsSheet.getLastColumn()).getValues()[0] + const bookingsSheetNextRow = bookingsSheet.getLastRow() + 1 + + const timestamp = Utilities.formatDate(new Date(), TIME_ZONE, TIMESTAMP_FORMAT) + const bookingId = createSignature(Date.now(), PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID'), 6) + const reference = createSignature( + formObject.find(entry => entry[0] === 'email')?.[1], + PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID')) + + const origin = formObject.find(entry => entry[0] === 'origin')?.[1] + const phone = `'${formObject.find(entry => entry[0] === 'phone')?.[1]}`.trim() + let bookings = [] + + // create a full record for every booked event + formObject.filter(param => param[0] === 'eventId').forEach(param => { + const [, eventId] = param + bookings.push(bookingsSheetHeaders.map((header) => { + if (header === 'timestamp') return timestamp + else if (header === 'id') return bookingId + else if (header === 'eventId') return eventId + else if (header === 'phone') return phone + else if (header === 'grade') return `${formObject.find(entry => entry[0] === 'grade')?.[1]}. Klasse` + else if (header === 'eventName') return `=XLOOKUP(INDIRECT(CONCATENATE("${BOOKINGS_SHEET_EVENT_ID_COLUMN}",ROW())),${EVENTS_SHEET_NAME}!${EVENTS_SHEET_ID_COLUMN},${EVENTS_SHEET_NAME}!${EVENTS_SHEET_NAME_COLUMN},"")` + else if (header === 'waitingList') return `=IF((XLOOKUP(INDIRECT(CONCATENATE("${BOOKINGS_SHEET_EVENT_ID_COLUMN}",ROW())),${EVENTS_SHEET_NAME}!${EVENTS_SHEET_ID_COLUMN},${EVENTS_SHEET_NAME}!${EVENTS_SHEET_PARTICIPANT_LIMIT_COLUMN},""))-(COUNTIF(${BOOKINGS_SHEET_EVENT_ID_COLUMN}2:INDIRECT(CONCATENATE("${BOOKINGS_SHEET_EVENT_ID_COLUMN}",ROW())),INDIRECT(CONCATENATE("${BOOKINGS_SHEET_EVENT_ID_COLUMN}",ROW())))) < 0, "Ja", "Nein")` + else if (header === 'costs') return `=XLOOKUP(INDIRECT(CONCATENATE("${BOOKINGS_SHEET_EVENT_ID_COLUMN}",ROW())),${EVENTS_SHEET_NAME}!${EVENTS_SHEET_ID_COLUMN},${EVENTS_SHEET_NAME}!${EVENTS_SHEET_COSTS_COLUMN},"")` + else if (header === 'additionalCosts') return `=XLOOKUP(INDIRECT(CONCATENATE("${BOOKINGS_SHEET_EVENT_ID_COLUMN}",ROW())),${EVENTS_SHEET_NAME}!${EVENTS_SHEET_ID_COLUMN},${EVENTS_SHEET_NAME}!${EVENTS_SHEET_ADDITIONAL_COSTS_COLUMN},"")` + else if (header === 'reference') return reference + else if (header === 'url') return getStatusUrl(origin, reference) + else return formObject.find(entry => entry[0] === header)?.[1] + })) + }) + + bookingsSheet.getRange(bookingsSheetNextRow, 1, bookings.length, bookingsSheetHeaders.length).setValues(bookings) + + // add optional volunteers + const volunteeringId = formObject.find(entry => entry[0] === 'volunteering')?.[1] + if (volunteeringId) { + const volunteersSheet = spreadsheet.getSheetByName(VOLUNTEERS_SHEET_NAME) + const volunteersSheetHeaders = volunteersSheet.getRange(1, 1, 1, volunteersSheet.getLastColumn()).getValues()[0] + const volunteersSheetNextRow = volunteersSheet.getLastRow() + 1 + + volunteersSheet.getRange(volunteersSheetNextRow, 1, 1, volunteersSheetHeaders.length).setValues( + [volunteersSheetHeaders.map((header) => { + if (header === 'phone') return phone + else if (header === 'option') return volunteeringId + else if (header === 'volunteering') return `=LOOKUP(INDIRECT(CONCATENATE("B",ROW())),Volunteering!A:A,Volunteering!B:B)` + else return formObject.find(entry => entry[0] === header)?.[1] + })] + ) + } + + // update caches + CacheService.getScriptCache().put(EVENT_CACHE_IDENTIFIER, getDataFromSheet(spreadsheet, EVENTS_SHEET_NAME), CACHE_EXPIRATION_TIMEOUT) + CacheService.getScriptCache().put(BOOKINGS_CACHE_IDENTIFIER, getDataFromSheet(spreadsheet, BOOKINGS_SHEET_NAME), CACHE_EXPIRATION_TIMEOUT) + + // optional machen, oder try catch + // unhandled exceptions per e-mail + const emailTo = formObject.find(entry => entry[0] === 'email')?.[1] + const firstName = formObject.find(entry => entry[0] === 'firstName')?.[1] + const htmlBody = ` + + + Logo Ferienpass Seeberg +

Hallo ${firstName} 👋🏻

+

Vielen herzlichen Dank für deine Anmeldung beim Ferienpass Seeberg. Du kannst + hier jederzeit + den Status deiner gebuchten Kurse überprüfen. Die Rechnung, unter Berücksichtigung + der im Programmheft beschriebenen Familienpauschale, erhältst du nach Anmeldeschluss.

+

Hier kannst du weitere Kurse buchen.

+

Tschüss und bis bald

+

Dein Ferienpass Seeberg Team

+ + + ` + + const textBody = ` + Hallo ${firstName} + + Vielen herzlichen Dank für deine Anmeldung beim Ferienpass Seeberg. Unter dem nachfolgenden + Link kannst du jederzeit den Status deiner gebuchten Kurse ueberpruefen. + + ${getStatusUrl(origin, reference)} + + Die Rechnung, unter Beruecksichtigung der im Programmheft beschriebenen Familienpauschale, + erhaeltst du nach Anmeldeschluss. + + Hier kannst du weitere Kurse buchen. + + ${getPreFilledFormUrl(origin, bookingId)} + + Tschuess und bis bald. + Dein Ferienpass Seeberg Team + ` + sendMail({ + to: emailTo, + emailFrom: PropertiesService.getScriptProperties().getProperty('EMAIL_FROM_ADDRESS'), + nameFrom: PropertiesService.getScriptProperties().getProperty('EMAIL_FROM_NAME'), + subject: "Deine Anmeldung beim Ferienpass Seeberg 🎉", + textBody: textBody, + htmlBody: htmlBody, + }) + + // return redirect url + return `${origin}?action=success&bookingId=${bookingId}` + } + catch (error) { + errorMsg = `${error}
${error.stack}

RequestData:

${formObject}` + sendMail({ + to: PropertiesService.getScriptProperties().getProperty('EMAIL_SUPPORT_REQUESTS'), + emailFrom: PropertiesService.getScriptProperties().getProperty('EMAIL_FROM_ADDRESS'), + nameFrom: PropertiesService.getScriptProperties().getProperty('EMAIL_FROM_NAME'), + subject: "Fehler im Ferienpass Backend 🙈", + textBody: errorMsg, + htmlBody: errorMsg, + }) + + throw error + } + finally { lock.releaseLock() } +} + +const getDataFromSheet = (spreadsheet, sheetName) => { + const sheet = spreadsheet.getSheetByName(sheetName) + let numRows = sheet.getLastRow() - 1 + if (numRows === 0) numRows = 1 + return JSON.stringify( + sheet.getRange(2, 1, numRows, sheet.getLastColumn()).getValues() + ) +} + +/** Creates a custom signature for the given value and key. */ +const createSignature = (value, key, length) => { + const signature = Utilities.computeHmacSignature( + Utilities.MacAlgorithm.HMAC_MD5, + value, + key, + Utilities.Charset.US_ASCII); + return Utilities.base64EncodeWebSafe(signature) + .replace(/[_\-=]+/g, '') + .substring(0, length ?? 12) +} + +const updateCache = (identifier, sheetName) => { + let result + const lock = LockService.getScriptLock() + const hasLock = lock.tryLock(SCRIPT_LOCK_TIMEOUT) + if (!hasLock) { throw new Error('Could not obtain lock after 40 seconds.') } + try { + const spreadsheet = SpreadsheetApp.openById( + PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID')) + const data = getDataFromSheet(spreadsheet, sheetName) + CacheService.getScriptCache().put(identifier, data, CACHE_EXPIRATION_TIMEOUT) + result = data + } + catch (error) { throw error } + finally { lock.releaseLock() } + return result +} + +// Get the status url +const getStatusUrl = (baseUrl, reference) => `${baseUrl}?action=status&reference=${reference}` + +// Get the pre-filled form url +const getPreFilledFormUrl = (baseUrl, bookingId) => `${baseUrl}?bookingId=${bookingId}` + +// Send an email +const sendMail = email => Gmail.Users.Messages.send({ raw: convertToGmailMessage(email) }, "me"); +const convertToGmailMessage = ({ to, emailFrom, nameFrom, subject, textBody, htmlBody }) => { + const boundary = "boundaryboundary"; + const mailData = [ + `MIME-Version: 1.0`, + `To: ${to}`, + nameFrom && emailFrom ? `From: "${nameFrom}" <${emailFrom}>` : "", + `Subject: =?UTF-8?B?${Utilities.base64Encode( + subject, + Utilities.Charset.UTF_8 + )}?=`, + `Content-Type: multipart/alternative; boundary=${boundary}`, + ``, + `--${boundary}`, + `Content-Type: text/plain; charset=UTF-8`, + ``, + textBody, + ``, + `--${boundary}`, + `Content-Type: text/html; charset=UTF-8`, + `Content-Transfer-Encoding: base64`, + ``, + Utilities.base64Encode(htmlBody, Utilities.Charset.UTF_8), + ``, + `--${boundary}--`, + ].join("\r\n"); + return Utilities.base64EncodeWebSafe(mailData); +}; diff --git a/src/appsscript.json b/src/appsscript.json new file mode 100644 index 0000000..723f797 --- /dev/null +++ b/src/appsscript.json @@ -0,0 +1,23 @@ +{ + "timeZone": "Europe/Zurich", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Gmail", + "version": "v1", + "serviceId": "gmail" + } + ] + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "webapp": { + "executeAs": "USER_DEPLOYING", + "access": "ANYONE_ANONYMOUS" + }, + "oauthScopes": [ + "https://www.googleapis.com/auth/spreadsheets.currentonly", + "https://www.googleapis.com/auth/spreadsheets", + "https://www.googleapis.com/auth/gmail.send" + ] +} diff --git a/src/css.html b/src/css.html new file mode 100644 index 0000000..e5bd700 --- /dev/null +++ b/src/css.html @@ -0,0 +1,131 @@ + diff --git a/src/head.html b/src/head.html new file mode 100644 index 0000000..3c6c628 --- /dev/null +++ b/src/head.html @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..5c3fac4 --- /dev/null +++ b/src/index.html @@ -0,0 +1,281 @@ + + + + + + + + + +
+ +
+
+
+
+ + +
+

Anmeldung Ferienpass Seeberg

+
+

 Allgemeine Informationen

+
+
    +
  • Der + Anmeldeschluss ist Sonntag, 21. Mai 2023 um 24.00 Uhr.
  • +
  • Du kannst dich + für all diejenigen Kurse anmelden, welche deiner Klasse im neuen Schuljahr (2023/24) entsprechend + ausgeschrieben sind.
  • +
  • Falls ein + gewünschter Kurs bereits ausgebucht ist, kannst du dich trotzdem anmelden. Du landest dann + auf der Warteliste und rutschst automatisch nach, falls ein Platz frei werden sollte oder wir + kurzfristig noch mehr Plätze anbieten können.
  • +
  • Bitte verwende + für alle Familienmitglieder dieselbe E-Mail-Adresse. So können wir sicherstellen, dass + die Familienpauschale korrekt berechnet wird.
  • +
+
+

🏸 Kursangebot + in der Woche vom 7. - 11. August 2023 +

+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + +
Nr.BeschreibungDetailsStatus
+

  Das Kursangebot wird geladen...

+
+
+ +
+

🗒️ Anmeldeinformationen

+ +
+ + +
+ Bitte Vorname angeben. +
+
+ +
+ + +
+ Bitte Nachnamen angeben. +
+
+ +
+ + +
+ Bitte Strasse und Hausnummer abgeben. +
+
+ +
+ + +
+ Bitte Postleitzahl angeben. +
+
+ +
+ + +
+ Bitte Wohnort abgeben. +
+
+ +
+ + +
+ Bitte gib eine gültige E-Mail Adresse an. +
+
+ +
+ + +
+ Bitte gib eine gültige Telefonnummer im Format '079 999 99 99' an. +
+
+ +
+ + +
+ Bitte Klasse angeben. +
+
+ +
+ + +
+ Bitte gib ein gültiges Geburtsdatum an. +
+
+ +
+

+ 👷🏽 Freiwillige Mithilfe am Sommerfest + vom Freitag 11. August 2023 +

+ +

Für die Durchführung des Sommerfests sind wir auf deine Mithilfe angewiesen. Ein Einsatz dauert, abhängig + von der Anzahl gemeldeter Helferinnen und Helfer, zwischen einer bis maximal drei Stunden. Bei folgenden + Arbeiten können wir deine Unterstützung gebrauchen:

+ +
    +
  1. +
    +
    Festaufbau
    + Du hilfst uns am Freitagnachmittag ab ca. 14.00 Uhr beim Aufbau und Einrichten. +
    +
  2. +
  3. +
    +
    Festbetrieb
    + Du hilfst uns während des Festbetriebs am Abend zwischen 18.00 und 22.00 Uhr. +
    +
  4. +
  5. +
    +
    Festabbau
    + Du hilfst uns beim Abbau und Aufräumen nach dem Fest ab ca. 22.00 Uhr. +
    +
  6. +
+ +

Detaillierte Informationen zu deinem gewählten Einsatz erhältst du von uns per E-Mail. Vielen herzlichen + Dank für deine Unterstützung 🙏🏻

+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + +
+ Bitte bestätige die verbindliche Buchung der oben ausgewählten Kurse. +
+
+
+
+
+ +
+
+
+ + + + + + +
+ + + + + + + diff --git a/src/javascript.html b/src/javascript.html new file mode 100644 index 0000000..cfd3a1c --- /dev/null +++ b/src/javascript.html @@ -0,0 +1,206 @@ + diff --git a/src/status.html b/src/status.html new file mode 100644 index 0000000..9ac9154 --- /dev/null +++ b/src/status.html @@ -0,0 +1,64 @@ + + + + + + + + + +
+ +
+
+
+
+

Deine gebuchten Kurse

+ + + + + + + + + + + +
NameKursStatus
+
+
+ + + + + + + + + + diff --git a/src/success.html b/src/success.html new file mode 100644 index 0000000..b293b3c --- /dev/null +++ b/src/success.html @@ -0,0 +1,37 @@ + + + + + + + + + +
+ +
+
+
+
+

Deine Anmeldung war erfolgreich 🎉

+

Vielen herzlichen Dank für deine Anmeldung beim Ferienpass Seeberg. Wir freuen uns sehr, dass du + beim Ferienpass dabei bist. 🥳 Wir haben dir per E-Mail soeben einen Link geschickt. Dort kannst du jederzeit + den Status deiner gebuchten Kurse überprüfen.

+

Hier kannst + du weitere Kurse buchen.

+

Tschüss und bis bald 👋

+

Dein Ferienpass Seeberg Team

+
+
+ + + + + + + + + diff --git a/src/theme.html b/src/theme.html new file mode 100644 index 0000000..3754ea0 --- /dev/null +++ b/src/theme.html @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +