First commit
This commit is contained in:
commit
f83a061e39
9 changed files with 341 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
credentials.json
|
||||||
16
Dockerfile
Normal file
16
Dockerfile
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose the port Flask runs on
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Run with Gunicorn (Production Server)
|
||||||
|
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"]
|
||||||
83
app.py
Normal file
83
app.py
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
import gspread
|
||||||
|
from flask import Flask, render_template, request, redirect, url_for, abort
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Load Configuration
|
||||||
|
def load_config():
|
||||||
|
with open('events.yaml', 'r') as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
|
||||||
|
# Connect to Google Sheets
|
||||||
|
def get_google_sheet(sheet_id):
|
||||||
|
gc = gspread.service_account(filename='credentials.json')
|
||||||
|
try:
|
||||||
|
sh = gc.open_by_key(sheet_id)
|
||||||
|
return sh.sheet1 # Assumes data goes into the first tab
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error connecting to Google Sheet: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def home():
|
||||||
|
# List all available active events
|
||||||
|
config = load_config()
|
||||||
|
events = config.get('events', {})
|
||||||
|
return render_template('home.html', events=events)
|
||||||
|
|
||||||
|
# Dynamic Route for any event defined in YAML
|
||||||
|
@app.route('/<event_slug>', methods=['GET', 'POST'])
|
||||||
|
def event_form(event_slug):
|
||||||
|
config = load_config()
|
||||||
|
events = config.get('events', {})
|
||||||
|
|
||||||
|
# Check if event exists in config
|
||||||
|
if event_slug not in events:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
event_data = events[event_slug]
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
# 1. Collect Form Data
|
||||||
|
form_data = [
|
||||||
|
datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # Timestamp
|
||||||
|
request.form.get('klasse'),
|
||||||
|
request.form.get('zeilnummer'),
|
||||||
|
request.form.get('bootnaam'),
|
||||||
|
request.form.get('boottype'),
|
||||||
|
# Checkboxes (Join them or separate columns? Let's join for simplicity)
|
||||||
|
", ".join([k for k in ['genua', 'rolfok', 'spinaker', 'halfwinder', 'genaker', 'dacron'] if k in request.form]),
|
||||||
|
request.form.get('schroef'),
|
||||||
|
request.form.get('naam'),
|
||||||
|
request.form.get('straat'),
|
||||||
|
request.form.get('postcode'),
|
||||||
|
request.form.get('plaats'),
|
||||||
|
request.form.get('land'),
|
||||||
|
request.form.get('telefoonmobiel'),
|
||||||
|
request.form.get('email'),
|
||||||
|
request.form.get('startlicentienummer'),
|
||||||
|
request.form.get('vereniging'),
|
||||||
|
request.form.get('opmerkingen')
|
||||||
|
]
|
||||||
|
|
||||||
|
# 2. Push to Google Sheet
|
||||||
|
sheet = get_google_sheet(event_data['sheet_id'])
|
||||||
|
if sheet:
|
||||||
|
sheet.append_row(form_data)
|
||||||
|
return redirect(url_for('success', event_slug=event_slug))
|
||||||
|
else:
|
||||||
|
return "Error: Could not connect to Google Sheet. Check server logs."
|
||||||
|
|
||||||
|
return render_template('form.html', event=event_data, slug=event_slug)
|
||||||
|
|
||||||
|
@app.route('/<event_slug>/success')
|
||||||
|
def success(event_slug):
|
||||||
|
config = load_config()
|
||||||
|
event_data = config['events'].get(event_slug)
|
||||||
|
return render_template('success.html', event=event_data)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=5000)
|
||||||
16
docker-compose.yaml
Normal file
16
docker-compose.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
sailing-forms:
|
||||||
|
build: .
|
||||||
|
container_name: sailing_forms
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
# Mount the config file so you can edit it without rebuilding
|
||||||
|
- ./events.yaml:/app/events.yaml
|
||||||
|
# Mount credentials securely
|
||||||
|
- ./credentials.json:/app/credentials.json
|
||||||
|
|
||||||
|
|
||||||
|
networks:
|
||||||
|
npm_network:
|
||||||
|
external: true
|
||||||
18
events.yaml
Normal file
18
events.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Define as many events as you want here.
|
||||||
|
# The key (e.g., 'zomeravond') becomes the URL: domain.com/zomeravond
|
||||||
|
|
||||||
|
events:
|
||||||
|
zomeravond:
|
||||||
|
title: "Inschrijving Zomeravondregatta"
|
||||||
|
sheet_id: "1k-eTke2GGmcMtq2-acvWPvjgfDeUHBsR0_Bd_tAbLx0"
|
||||||
|
description: "De gezelligste avondwedstrijd van het jaar."
|
||||||
|
|
||||||
|
papklokken:
|
||||||
|
title: "Papklokkenrace 2025"
|
||||||
|
sheet_id: "1k-eTke2GGmcMtq2-acvWPvjgfDeUHBsR0_Bd_tAbLx0"
|
||||||
|
description: "Sluit het seizoen af in stijl."
|
||||||
|
|
||||||
|
winterwedstrijd:
|
||||||
|
title: "Winter Bokaal"
|
||||||
|
sheet_id: "1k-eTke2GGmcMtq2-acvWPvjgfDeUHBsR0_Bd_tAbLx0"
|
||||||
|
description: "Alleen voor de echte bikkels."
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
Flask==3.0.0
|
||||||
|
gspread==5.12.0
|
||||||
|
PyYAML==6.0.1
|
||||||
|
gunicorn==21.2.0
|
||||||
140
templates/form.html
Normal file
140
templates/form.html
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ event.title }}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body { background-color: #f0f2f5; }
|
||||||
|
.form-card { max-width: 800px; margin: 40px auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
|
||||||
|
.section-title { border-bottom: 2px solid #0d6efd; padding-bottom: 10px; margin-bottom: 20px; color: #0d6efd; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="form-card">
|
||||||
|
<h1 class="text-center mb-2">{{ event.title }}</h1>
|
||||||
|
<p class="text-center text-muted mb-4">{{ event.description }}</p>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<!-- Boot Info -->
|
||||||
|
<h4 class="section-title">De Boot</h4>
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Klasse</label>
|
||||||
|
<select class="form-select" name="klasse" required>
|
||||||
|
<option value="" disabled selected>Kies...</option>
|
||||||
|
<option>Kajuitklasse</option>
|
||||||
|
<option>Openmeermans</option>
|
||||||
|
<option>Openeenmans</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Zeilnummer</label>
|
||||||
|
<input type="text" class="form-control" name="zeilnummer" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Bootnaam</label>
|
||||||
|
<input type="text" class="form-control" name="bootnaam" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Type</label>
|
||||||
|
<input type="text" class="form-control" name="boottype">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zeilvoering -->
|
||||||
|
<h4 class="section-title mt-4">Zeilvoering</h4>
|
||||||
|
<div class="row mb-3">
|
||||||
|
{% for sail in ['Genua', 'Rolfok', 'Spinaker', 'Halfwinder', 'Genaker', 'Alleen dacron'] %}
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="{{ sail|lower|replace(' ', '') }}" id="sail{{ loop.index }}">
|
||||||
|
<label class="form-check-label" for="sail{{ loop.index }}">{{ sail }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Motor -->
|
||||||
|
<h4 class="section-title mt-4">Motor / Schroef</h4>
|
||||||
|
<div class="mb-3">
|
||||||
|
<select class="form-select" name="schroef">
|
||||||
|
<option>Geen motor</option>
|
||||||
|
<option>Buitenboordmotor uit het water</option>
|
||||||
|
<option>Buitenboordmotor in bun</option>
|
||||||
|
<option>Vaste schroef</option>
|
||||||
|
<option>Vaanstand/klap schroef</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Person Info -->
|
||||||
|
<h4 class="section-title mt-4">Schipper / Eigenaar</h4>
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Naam</label>
|
||||||
|
<input type="text" class="form-control" name="naam" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label">Straat + Huisnr</label>
|
||||||
|
<input type="text" class="form-control" name="straat">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Postcode</label>
|
||||||
|
<input type="text" class="form-control" name="postcode">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Plaats</label>
|
||||||
|
<input type="text" class="form-control" name="plaats">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Land</label>
|
||||||
|
<input type="text" class="form-control" name="land" value="Nederland">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Telefoon (06)</label>
|
||||||
|
<input type="text" class="form-control" name="telefoonmobiel" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Email</label>
|
||||||
|
<input type="email" class="form-control" name="email" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Vereniging</label>
|
||||||
|
<input type="text" class="form-control" name="vereniging">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Licentienummer</label>
|
||||||
|
<input type="text" class="form-control" name="startlicentienummer">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">Opmerkingen</label>
|
||||||
|
<textarea class="form-control" name="opmerkingen" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Terms -->
|
||||||
|
<div class="alert alert-secondary text-small" style="font-size: 0.85rem; max-height: 150px; overflow-y: auto;">
|
||||||
|
<strong>Voorwaarden:</strong><br>
|
||||||
|
Deelnemers nemen deel aan dit evenement voor geheel eigen risico.
|
||||||
|
De organiserende vereniging kan nimmer aansprakelijk gesteld worden.
|
||||||
|
(Zie wedstrijdreglement voor volledige tekst).
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-4">
|
||||||
|
<input class="form-check-input" type="checkbox" required id="terms">
|
||||||
|
<label class="form-check-label" for="terms">Ik ga akkoord met de voorwaarden.</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">Inschrijven</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
43
templates/participants.html
Normal file
43
templates/participants.html
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Deelnemerslijst</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light p-4">
|
||||||
|
<div class="container bg-white p-4 rounded shadow">
|
||||||
|
<h2 class="mb-4 text-primary">Huidige Deelnemers</h2>
|
||||||
|
<a href="/" class="btn btn-outline-secondary mb-4">← Terug naar inschrijving</a>
|
||||||
|
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Klasse</th>
|
||||||
|
<th>Zeilnummer</th>
|
||||||
|
<th>Bootnaam</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Schipper</th>
|
||||||
|
<th>Vereniging</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for boat in boats %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ boat.klasse }}</td>
|
||||||
|
<td>{{ boat.zeilnummer }}</td>
|
||||||
|
<td>{{ boat.bootnaam }}</td>
|
||||||
|
<td>{{ boat.boottype }}</td>
|
||||||
|
<td>{{ boat.naam }}</td>
|
||||||
|
<td>{{ boat.vereniging }}</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center">Nog geen inschrijvingen.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
templates/success.html
Normal file
20
templates/success.html
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Bedankt!</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light text-center" style="padding-top: 100px;">
|
||||||
|
<div class="container">
|
||||||
|
<div class="card shadow p-5 mx-auto" style="max-width: 600px;">
|
||||||
|
<h1 class="text-success mb-4">Bedankt!</h1>
|
||||||
|
<p class="lead">Je inschrijving voor <strong>{{ event.title }}</strong> is ontvangen.</p>
|
||||||
|
<p>De gegevens zijn opgeslagen.</p>
|
||||||
|
<hr>
|
||||||
|
<a href="https://docs.google.com/spreadsheets/d/{{ event.sheet_id }}" target="_blank" class="btn btn-outline-success">Bekijk deelnemers (Google Sheets)</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue