first commit

This commit is contained in:
2026-01-21 15:26:12 +01:00
parent 869fedd0a7
commit 1b14b07fea
36 changed files with 2804 additions and 0 deletions

12
Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM python:3.11-slim
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["gunicorn", "wsgi:app", "-b", "0.0.0.0:5000"]

7
ad.py Normal file
View File

@@ -0,0 +1,7 @@
from app import db, Liturgiebord
import uuid
with db.app.app_context():
for board in Liturgiebord.query.filter((Liturgiebord.unique_id == None) | (Liturgiebord.unique_id == '')).all():
board.unique_id = uuid.uuid4().hex
db.session.commit()

688
app.py Normal file
View File

@@ -0,0 +1,688 @@
from flask import Flask, render_template, redirect, url_for, request, flash, session
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, login_user, login_required, logout_user, current_user, UserMixin, AnonymousUserMixin
import os
from werkzeug.security import generate_password_hash, check_password_hash
from flask import send_from_directory
from werkzeug.utils import secure_filename
from flask_migrate import Migrate
import uuid
UPLOAD_FOLDER = 'static/uploads'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key_here'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///liturgie.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
db = SQLAlchemy(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
migrate = Migrate(app, db)
# Models
class Church(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(150), unique=True, nullable=False)
is_active = db.Column(db.Boolean, default=False, nullable=False) # New field for activation state
contact_email = db.Column(db.String(150), default='')
contact_phone = db.Column(db.String(50), default='')
contact_address = db.Column(db.String(200), default='')
logo_filename = db.Column(db.String(300), nullable=True) # New field for logo filename
users = db.relationship('User', backref='church', lazy=True)
boards = db.relationship('Liturgiebord', backref='church', lazy=True)
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(150), unique=True, nullable=False)
password = db.Column(db.String(150), nullable=False)
church_id = db.Column(db.Integer, db.ForeignKey('church.id'), nullable=False)
is_admin = db.Column(db.Boolean, default=False)
# Association table for many-to-many relationship
board_schedule_association = db.Table('board_schedule',
db.Column('board_id', db.Integer, db.ForeignKey('liturgiebord.id'), primary_key=True),
db.Column('schedule_id', db.Integer, db.ForeignKey('schedule.id'), primary_key=True)
)
class Liturgiebord(db.Model):
id = db.Column(db.Integer, primary_key=True)
unique_id = db.Column(db.String(32), unique=True, nullable=False, default=lambda: uuid.uuid4().hex)
name = db.Column(db.String(150), nullable=False)
church_id = db.Column(db.Integer, db.ForeignKey('church.id'), nullable=False)
line1 = db.Column(db.String(200), default='')
line2 = db.Column(db.String(200), default='')
line3 = db.Column(db.String(200), default='')
line4 = db.Column(db.String(200), default='')
line5 = db.Column(db.String(200), default='')
line6 = db.Column(db.String(200), default='')
line7 = db.Column(db.String(200), default='')
line8 = db.Column(db.String(200), default='')
line9 = db.Column(db.String(200), default='')
line10 = db.Column(db.String(200), default='')
background_image = db.Column(db.String(300), default=None)
# Updated relationship: many-to-many
schedules = db.relationship('Schedule', secondary=board_schedule_association, back_populates='boards', lazy='dynamic')
class Schedule(db.Model):
id = db.Column(db.Integer, primary_key=True)
# Removed board_id to make schedules global
name = db.Column(db.String(150), nullable=False)
start_time = db.Column(db.Time, nullable=False)
end_time = db.Column(db.Time, nullable=False)
date = db.Column(db.Date, nullable=False)
content = db.Column(db.Text, nullable=False) # separate lines with \n
# Many-to-many relationship back to boards
boards = db.relationship('Liturgiebord', secondary=board_schedule_association, back_populates='schedules', lazy='dynamic')
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
from flask_login import current_user
@app.route('/')
def index():
if current_user.is_authenticated:
return redirect(url_for('portal'))
return render_template('index.html')
# --- Church Settings Route ---
from werkzeug.security import generate_password_hash
from werkzeug.utils import secure_filename
@app.route('/church/settings', methods=['GET', 'POST'])
@login_required
def church_settings():
church = current_user.church
msg = None
if request.method == 'POST' and request.form.get('form_type') == 'contact':
church.name = request.form.get('name', '').strip()
church.contact_email = request.form.get('contact_email', '').strip()
church.contact_phone = request.form.get('contact_phone', '').strip()
church.contact_address = request.form.get('contact_address', '').strip()
# Handle logo upload
if 'logo' in request.files:
logo = request.files['logo']
if logo and allowed_file(logo.filename):
# Remove old logo file if exists
if church.logo_filename:
old_filepath = os.path.join(app.config['UPLOAD_FOLDER'], church.logo_filename)
if os.path.exists(old_filepath):
os.remove(old_filepath)
# Create a unique filename
import time
filename = secure_filename(f'church_{church.id}_logo_{int(time.time())}_{logo.filename}')
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
logo.save(filepath)
church.logo_filename = filename
db.session.commit()
msg = 'Kerkgegevens opgeslagen!'
elif request.method == 'POST' and request.form.get('form_type') == 'user':
username = request.form.get('username', '').strip()
password = request.form.get('password', '').strip()
if not username or not password:
msg = 'Gebruikersnaam en wachtwoord zijn verplicht.'
elif User.query.filter_by(username=username).first():
msg = 'Gebruikersnaam bestaat al.'
else:
user = User(username=username, password=generate_password_hash(password), church_id=church.id)
db.session.add(user)
db.session.commit()
msg = f'Gebruiker {username} toegevoegd aan deze kerk.'
users = church.users
return render_template('church_settings.html', church=church, users=users, msg=msg)
# Placeholder routes for login, register, church portal, board management, and display
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('portal'))
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = User.query.filter_by(username=username).first()
if user and check_password_hash(user.password, password):
login_user(user)
return redirect(url_for('portal'))
else:
flash('Ongeldige gebruikersnaam of wachtwoord')
return render_template('login.html')
@app.route('/church/delete_user/<int:user_id>', methods=['POST'])
@login_required
def church_delete_user(user_id):
user_to_delete = User.query.get_or_404(user_id)
# Confirm user belongs to the same church
if user_to_delete.church_id != current_user.church_id:
flash('Geen toegang tot deze gebruiker')
return redirect(url_for('church_settings'))
# Disallow deleting self
if user_to_delete.id == current_user.id:
flash('Je kunt jezelf niet verwijderen')
return redirect(url_for('church_settings'))
# Disallow deleting admin users
if user_to_delete.is_admin:
flash('Kan admin-gebruiker niet verwijderen')
return redirect(url_for('church_settings'))
db.session.delete(user_to_delete)
db.session.commit()
flash(f'Gebruiker {user_to_delete.username} verwijderd!')
return redirect(url_for('church_settings'))
@app.route('/logout')
@login_required
def logout():
session.pop('impersonate_id', None)
session.pop('admin_id', None)
logout_user()
return redirect(url_for('login'))
@app.route('/register', methods=['GET', 'POST'])
def register():
import re
if current_user.is_authenticated:
return redirect(url_for('portal'))
if request.method == 'POST':
email = request.form['username'].strip()
password = request.form['password']
church_name = request.form['church'].strip()
# Validate basic email format
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
flash('Voer een geldig e-mailadres in')
return render_template('register.html')
if User.query.filter_by(username=email).first():
flash('Emailadres bestaat al')
return render_template('register.html')
church = Church.query.filter_by(name=church_name).first()
if church:
# Prevent registering to an existing church to avoid security risk
flash('Het church name bestaat al. Maak een unieke church aan.')
return render_template('register.html')
else:
church = Church(name=church_name, is_active=False) # New churches start inactive
db.session.add(church)
db.session.commit()
hashed_pw = generate_password_hash(password)
user = User(username=email, password=hashed_pw, church_id=church.id)
db.session.add(user)
db.session.commit()
flash('Registratie gelukt! Je kunt nu inloggen.')
return redirect(url_for('login'))
return render_template('register.html')
@app.route('/portal')
@login_required
def portal():
boards = Liturgiebord.query.filter_by(church_id=current_user.church_id).all()
return render_template('portal.html', boards=boards)
@app.route('/board/add', methods=['GET', 'POST'])
@login_required
def add_board():
if request.method == 'POST':
name = request.form.get('name')
board = Liturgiebord(name=name, church_id=current_user.church_id)
db.session.add(board)
db.session.commit()
return redirect(url_for('portal'))
return render_template('add_board.html')
@app.route('/board/<int:board_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_board(board_id):
board = Liturgiebord.query.get_or_404(board_id)
if board.church_id != current_user.church_id:
flash('Geen toegang tot dit bord')
return redirect(url_for('portal'))
if request.method == 'POST':
# If changing the name
if request.form.get('form_type') == 'name':
new_name = request.form.get('name', '').strip()
if new_name:
board.name = new_name
db.session.commit()
flash('Naam van het bord gewijzigd!')
else:
flash('Naam mag niet leeg zijn.')
return redirect(url_for('edit_board', board_id=board.id))
# Otherwise, update lines
for i in range(1, 11):
setattr(board, f'line{i}', request.form.get(f'line{i}', ''))
db.session.commit()
flash('Bord bijgewerkt!')
return redirect(url_for('portal'))
# Get schedules assigned to this board
schedules = board.schedules.order_by(Schedule.date.desc(), Schedule.start_time).all()
# Find the active schedule object
active_schedule = None
now = datetime.now()
today = now.date()
current_time = now.time()
for schedule in schedules:
if schedule.date == today and schedule.start_time <= current_time <= schedule.end_time:
active_schedule = schedule
break
schedule_active = active_schedule is not None
schedule_content = None
if schedule_active:
schedule_content = active_schedule.content
return render_template('edit_board.html', board=board, schedules=schedules, schedule_active=schedule_active, active_schedule=active_schedule, schedule_content=schedule_content)
from datetime import datetime
# Helper to get active schedule for a board
from sqlalchemy import and_, or_
def get_active_schedule(board):
now = datetime.now()
today = now.date()
current_time = now.time()
for schedule in board.schedules:
if schedule.date != today:
continue
if not (schedule.start_time <= current_time <= schedule.end_time):
continue
return schedule.content
return None
@app.route('/board/<int:board_id>')
def display_board(board_id):
board = Liturgiebord.query.get_or_404(board_id)
schedule_content = get_active_schedule(board)
return render_template('display_board.html', board=board, schedule_content=schedule_content)
from flask import jsonify
@app.route('/board/<int:board_id>/active_schedule_json')
def active_schedule_json(board_id):
board = Liturgiebord.query.get_or_404(board_id)
schedule_content = get_active_schedule(board)
# Return schedule lines if active, else board input lines
if schedule_content:
lines = schedule_content.split('\n')
else:
# Use board lines if no active schedule
lines = [
board.line1 or ' ',
board.line2 or ' ',
board.line3 or ' ',
board.line4 or ' ',
board.line5 or ' ',
board.line6 or ' ',
board.line7 or ' ',
board.line8 or ' ',
board.line9 or ' ',
board.line10 or ' '
]
# Also include background info for completeness
bg_url = None
if board.background_image:
# Determine URL path, check if default or uploaded
from flask import url_for
if board.background_image.startswith('default_'):
bg_url = url_for('static', filename=board.background_image)
else:
bg_url = url_for('uploaded_file', filename=board.background_image)
return jsonify({
'lines': lines,
'background': bg_url
})
@app.route('/board/<int:board_id>/delete', methods=['POST'])
@login_required
def delete_board(board_id):
board = Liturgiebord.query.get_or_404(board_id)
if board.church_id != current_user.church_id and not current_user.is_admin:
flash('Geen toegang tot dit bord')
return redirect(url_for('portal'))
# Remove schedule associations but keep schedules intact
board.schedules = []
db.session.commit()
# Then delete the board
db.session.delete(board)
db.session.commit()
flash('Liturgiebord verwijderd!')
return redirect(url_for('portal'))
from flask import abort
# Remove old per-board schedule management routes
@app.route('/schedules')
@login_required
def global_schedules():
user_church_id = current_user.church_id
# Non-admin: show only schedules linked to boards of their church
schedules = []
boards = Liturgiebord.query.filter_by(church_id=user_church_id).all()
board_ids = [b.id for b in boards]
all_schedules = Schedule.query.order_by(Schedule.date.desc(), Schedule.start_time).all()
for schedule in all_schedules:
# Check if schedule belongs to any board_id of current church
for board in schedule.boards:
if board.id in board_ids:
schedules.append(schedule)
break
boards = Liturgiebord.query.filter_by(church_id=user_church_id).all()
return render_template('global_schedules.html', schedules=schedules, boards=boards)
@app.route('/schedules/add', methods=['GET', 'POST'])
@login_required
def add_global_schedule():
user_church_id = current_user.church_id
boards = Liturgiebord.query.filter_by(church_id=user_church_id).all()
if request.method == 'POST':
from datetime import datetime
sched_date = datetime.strptime(request.form['date'], '%Y-%m-%d').date()
start_time = datetime.strptime(request.form['start_time'], '%H:%M').time()
end_time = datetime.strptime(request.form['end_time'], '%H:%M').time()
lines = []
for i in range(1, 11):
line = request.form.get(f'line{i}', '').strip()
if line == '':
line = ' '
lines.append(line)
content = '\n'.join(lines)
name = request.form['name']
board_ids = request.form.getlist('boards')
schedule_obj = Schedule(
name=name,
start_time=start_time,
end_time=end_time,
content=content,
date=sched_date
)
# Assign boards
for board_id in board_ids:
board = Liturgiebord.query.get(int(board_id))
if board and (board.church_id == user_church_id or current_user.is_admin):
schedule_obj.boards.append(board)
db.session.add(schedule_obj)
db.session.commit()
flash('Nieuwe planning toegevoegd.')
return redirect(url_for('global_schedules'))
return render_template('schedule_form.html', schedule=None, boards=boards)
@app.route('/schedules/<int:schedule_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_global_schedule(schedule_id):
user_church_id = current_user.church_id
schedule = Schedule.query.get_or_404(schedule_id)
# Convert boards dynamic relationship to list for template
schedule.boards_list = schedule.boards.all()
# Check user has access via any board or admin
accessible = False
for board in schedule.boards_list:
if board.church_id == user_church_id:
accessible = True
break
if not (accessible or current_user.is_admin):
abort(403)
boards = Liturgiebord.query.filter_by(church_id=user_church_id).all()
if request.method == 'POST':
from datetime import datetime
schedule.date = datetime.strptime(request.form['date'], '%Y-%m-%d').date()
schedule.start_time = datetime.strptime(request.form['start_time'], '%H:%M').time()
schedule.end_time = datetime.strptime(request.form['end_time'], '%H:%M').time()
lines = []
for i in range(1, 11):
line = request.form.get(f'line{i}', '').strip()
if line == '':
line = ' '
lines.append(line)
schedule.content = '\n'.join(lines)
schedule.name = request.form['name']
# Update board assignments
board_ids = request.form.getlist('boards')
schedule.boards = [] # Clear old assignments
for board_id in board_ids:
board = Liturgiebord.query.get(int(board_id))
if board and (board.church_id == user_church_id or current_user.is_admin):
schedule.boards.append(board)
db.session.commit()
flash('Planning bijgewerkt.')
return redirect(url_for('global_schedules'))
return render_template('schedule_form.html', schedule=schedule, boards=boards)
@app.route('/schedules/<int:schedule_id>/delete', methods=['POST'])
@login_required
def delete_global_schedule(schedule_id):
schedule = Schedule.query.get_or_404(schedule_id)
# Check user has access via any board or admin
user_church_id = current_user.church_id
accessible = False
for board in schedule.boards:
if board.church_id == user_church_id:
accessible = True
break
if not (accessible or current_user.is_admin):
abort(403)
db.session.delete(schedule)
db.session.commit()
flash('Planning verwijderd.')
return redirect(url_for('global_schedules'))
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/board/<int:board_id>/background', methods=['GET', 'POST'])
@login_required
def upload_background(board_id):
board = Liturgiebord.query.get_or_404(board_id)
if board.church_id != current_user.church_id:
flash('Geen toegang tot dit bord')
return redirect(url_for('portal'))
if request.method == 'POST':
# handle setting default wallpaper
default_background = request.form.get('default_background')
if default_background:
# Set the default wallpaper filename directly
# We assume the filename is safe and known
board.background_image = default_background
db.session.commit()
flash('Achtergrondafbeelding ingesteld!')
return redirect(url_for('edit_board', board_id=board.id))
if 'background' not in request.files:
flash('Geen bestand geselecteerd')
return redirect(request.url)
file = request.files['background']
if file and allowed_file(file.filename):
filename = secure_filename(f'board_{board.id}_bg_{file.filename}')
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
board.background_image = filename
db.session.commit()
flash('Achtergrondafbeelding bijgewerkt!')
return redirect(url_for('edit_board', board_id=board.id))
else:
flash('Ongeldig bestandstype')
return render_template('upload_background.html', board=board)
@app.route('/uploads/<filename>')
def uploaded_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
@app.route('/admin/login', methods=['GET', 'POST'])
def admin_login():
if current_user.is_authenticated and current_user.is_admin:
return redirect(url_for('admin_dashboard'))
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = User.query.filter_by(username=username, is_admin=True).first()
if user and check_password_hash(user.password, password):
login_user(user)
return redirect(url_for('admin_dashboard'))
else:
flash('Ongeldige admin-inloggegevens')
return render_template('admin_login.html')
from flask import request
@app.route('/admin/delete_church/<int:church_id>', methods=['POST'])
@login_required
def admin_delete_church(church_id):
if not current_user.is_admin:
flash('Geen toegang')
return redirect(url_for('portal'))
church = Church.query.get_or_404(church_id)
# Delete associated boards and users
for board in church.boards:
db.session.delete(board)
for user in church.users:
db.session.delete(user)
db.session.delete(church)
db.session.commit()
flash(f'Kerk {church.name} en alle bijbehorende data verwijderd!')
return redirect(url_for('admin_dashboard'))
@app.route('/admin/dashboard', methods=['GET', 'POST'])
@login_required
def admin_dashboard():
if not current_user.is_admin:
flash('Geen toegang')
return redirect(url_for('portal'))
if request.method == 'POST':
# Update church activation states
for church in Church.query.all():
checkbox_val = request.form.get(f'church_active_{church.id}')
church.is_active = checkbox_val == 'on'
db.session.commit()
flash('Kerken activatiestatus bijgewerkt.')
churches = Church.query.all()
users = User.query.all()
return render_template('admin_dashboard.html', churches=churches, users=users)
@app.route('/admin/impersonate/<int:user_id>')
@login_required
def admin_impersonate(user_id):
if not current_user.is_admin:
flash('Geen toegang')
return redirect(url_for('portal'))
user = User.query.get_or_404(user_id)
session['impersonate_id'] = user.id
session['admin_id'] = current_user.id
flash(f'Je bent nu ingelogd als {user.username}')
return redirect(url_for('portal'))
@app.before_request
def impersonate_user():
if 'impersonate_id' in session and 'admin_id' in session:
# Only impersonate if the real admin is logged in
admin = User.query.get(session['admin_id'])
if admin and admin.is_authenticated and admin.is_admin:
user = User.query.get(session['impersonate_id'])
if user:
# Use Flask-Login's login_user to switch context
login_user(user)
# Attach a proxy attribute to current_user to allow admin checks
current_user._is_impersonated = True
current_user._real_admin_id = admin.id
else:
# Not impersonating, ensure _is_impersonated is not set
if hasattr(current_user, '_is_impersonated'):
delattr(current_user, '_is_impersonated')
if hasattr(current_user, '_real_admin_id'):
delattr(current_user, '_real_admin_id')
@app.route('/admin/users')
@login_required
def admin_users():
if not current_user.is_admin:
flash('Geen toegang')
return redirect(url_for('portal'))
users = User.query.all()
return render_template('admin_users.html', users=users)
@app.route('/admin/delete_user/<int:user_id>', methods=['POST'])
@login_required
def admin_delete_user(user_id):
if not current_user.is_admin:
flash('Geen toegang')
return redirect(url_for('portal'))
user = User.query.get_or_404(user_id)
church = user.church
if user.is_admin:
flash('Kan geen admin-gebruiker verwijderen!')
return redirect(url_for('admin_users'))
if len(church.users) > 1:
# Just delete the user
db.session.delete(user)
db.session.commit()
flash(f'Gebruiker {user.username} verwijderd!')
else:
# Last user, delete user, church, boards
for board in church.boards:
db.session.delete(board)
db.session.delete(user)
db.session.delete(church)
db.session.commit()
flash(f'Laatste gebruiker verwijderd. Kerk en data ook verwijderd ({user.username}, {church.name})!')
return redirect(url_for('admin_users'))
@app.route('/display/<unique_id>')
def display_board_unique(unique_id):
board = Liturgiebord.query.filter_by(unique_id=unique_id).first_or_404()
schedule_content = get_active_schedule(board)
return render_template('display_board.html', board=board, schedule_content=schedule_content)
from werkzeug.security import check_password_hash
@app.route('/change_password', methods=['GET', 'POST'])
@login_required
def change_password():
msg = None
if request.method == 'POST':
current_password = request.form.get('current_password')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
if not check_password_hash(current_user.password, current_password):
msg = 'Huidig wachtwoord is onjuist.'
elif not new_password or not confirm_password:
msg = 'Voer een nieuw wachtwoord in.'
elif new_password != confirm_password:
msg = 'De nieuwe wachtwoorden komen niet overeen.'
else:
current_user.password = generate_password_hash(new_password)
db.session.commit()
msg = 'Wachtwoord succesvol gewijzigd.'
return render_template('change_password.html', msg=msg)
if __name__ == '__main__':
if not os.path.exists('liturgie.db'):
with app.app_context():
db.create_all()
# Create initial admin user if not exists
if not User.query.filter_by(username='admin').first():
admin_church = Church.query.filter_by(name='Admin').first()
if not admin_church:
admin_church = Church(name='Admin')
db.session.add(admin_church)
db.session.commit()
admin_user = User(username='admin', password=generate_password_hash('admin'), church_id=admin_church.id, is_admin=True)
db.session.add(admin_user)
db.session.commit()
app.run()

9
requirements.txt Normal file
View File

@@ -0,0 +1,9 @@
Flask
Flask_SQLAlchemy
Flask_Login
flask_migrate
Werkzeug
SQLAlchemy
itsdangerous
python-dotenv
gunicorn

BIN
static/Hero.jpg Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

BIN
static/default_logo.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
static/default_wall_1.jpg Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 KiB

BIN
static/default_wall_2.jpg Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
static/default_wall_3.jpg Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

BIN
static/default_wall_4.jpg Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
static/favicon.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
static/favicon.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
static/icons/board.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
static/icons/church.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
static/icons/cog.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
static/icons/group.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
static/icons/task.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

27
templates/add_board.html Normal file
View File

@@ -0,0 +1,27 @@
{% extends 'base.html' %}
{% block title %}Nieuw Liturgiebord Toevoegen - Digitale Liturgie{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<h2 class="text-3xl font-semibold mb-6">Nieuw Liturgiebord Toevoegen</h2>
<form method="post" class="space-y-6 max-w-xl">
<div>
<label for="name" class="block mb-3 font-semibold text-gray-700">Naam van het bord</label>
<input type="text" id="name" name="name" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div class="flex gap-4">
<button type="submit" class="bg-[#f7d91a] text-black px-8 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition">Toevoegen</button>
<a href="{{ url_for('portal') }}" class="bg-white text-black border border-black px-8 py-3 rounded-md font-semibold shadow hover:bg-gray-100 transition">Annuleren</a>
</div>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,107 @@
{% extends 'base.html' %}
{% block title %}Admin Dashboard - Digitale Liturgie{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<section>
<h2 class="text-3xl font-semibold mb-6">Admin Dashboard</h2>
<form method="POST" class="space-y-6">
<div>
<h3 class="text-2xl font-semibold mb-4">Kerken</h3>
<div class="overflow-x-auto rounded-lg border border-gray-300">
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th scope="col" class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Kerknaam</th>
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Actief</th>
<th scope="col" class="px-6 py-3 text-left text-sm font-semibold text-gray-700">URL</th>
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Acties</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for church in churches %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">{{ church.name }}</td>
<td class="text-center px-6 py-4">
<input type="checkbox" name="church_active_{{ church.id }}" {% if church.is_active %}checked{% endif %} class="h-5 w-5 text-blue-600">
</td>
<td class="px-6 py-4 text-sm text-blue-600">
{% if church.is_active %}
{% for board in church.boards %}
<div class="mb-1 break-all"><a href="{{ url_for('display_board_unique', unique_id=board.unique_id) }}" target="_blank" class="underline hover:text-blue-800 transition">{{ url_for('display_board_unique', unique_id=board.unique_id, _external=True) }}</a></div>
{% endfor %}
{% endif %}
</td>
<td class="text-center px-6 py-4">
<a href="#" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Weergeven</a>
<form action="{{ url_for('admin_delete_church', church_id=church.id) }}" method="post" style="display:inline;margin-left:8px;" onsubmit="return confirm('Weet je zeker dat je deze kerk en alle bijbehorende data wilt verwijderen?');">
<button type="submit" class="bg-red-600 text-white px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Verwijderen</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div>
<button type="submit" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition">Opslaan</button>
</div>
</form>
</section>
<section>
<h3 class="text-2xl font-semibold mb-4">Gebruikers</h3>
<div class="overflow-x-auto rounded-lg border border-gray-300">
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th scope="col" class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Gebruikersnaam</th>
<th scope="col" class="px-6 py-3 text-sm font-semibold text-gray-700">Kerk</th>
<th scope="col" class="px-6 py-3 text-sm font-semibold text-gray-700">Admin</th>
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Impersonalisatie</th>
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Acties</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for user in users %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">{{ user.username }}</td>
<td class="px-6 py-4 text-sm text-gray-900">{{ user.church.name }}</td>
<td class="px-6 py-4 text-sm text-gray-900">{% if user.is_admin %}Ja{% else %}Nee{% endif %}</td>
<td class="text-center px-6 py-4">
{% if not user.is_admin %}
<a href="{{ url_for('admin_impersonate', user_id=user.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Impersonate</a>
{% else %}-{% endif %}
</td>
<td class="text-center px-6 py-4">
{% if not user.is_admin %}
<form action="{{ url_for('admin_delete_user', user_id=user.id) }}" method="post" style="display:inline;" onsubmit="return confirm('Weet je zeker dat je deze gebruiker en alle bijbehorende kerkdata wilt verwijderen?');">
<button type="submit" class="bg-red-600 text-white text-xs px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Verwijderen</button>
</form>
{% else %}-{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
</div>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends 'base.html' %}
{% block title %}Admin Login - Digitale Liturgie{% endblock %}
{% block content %}
<div class="space-y-8 max-w-md mx-auto">
<h2 class="text-3xl font-semibold tracking-tight">Admin Login</h2>
<form method="post" class="space-y-6">
<div>
<label for="username" class="block mb-2 font-semibold text-gray-700">Gebruikersnaam</label>
<input type="text" id="username" name="username" required class="w-full rounded-md border border-gray-300 p-3 text-base placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="password" class="block mb-2 font-semibold text-gray-700">Wachtwoord</label>
<input type="password" id="password" name="password" required class="w-full rounded-md border border-gray-300 p-3 text-base placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<button type="submit" class="bg-blue-600 text-white w-full rounded-md py-3 font-semibold shadow hover:bg-blue-700 transition">Login</button>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,50 @@
{% extends 'base.html' %}
{% block title %}Gebruikersbeheer - Digitale Liturgie{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<h2 class="text-3xl font-semibold mb-6">Gebruikersbeheer</h2>
<div class="overflow-x-auto rounded-lg border border-gray-300">
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th scope="col" class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Gebruikersnaam</th>
<th scope="col" class="px-6 py-3 text-sm font-semibold text-gray-700">Kerk</th>
<th scope="col" class="px-6 py-3 text-sm font-semibold text-gray-700">Admin</th>
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Impersonalisatie</th>
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Acties</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for user in users %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">{{ user.username }}</td>
<td class="px-6 py-4 text-sm text-gray-900">{{ user.church.name }}</td>
<td class="px-6 py-4 text-sm text-gray-900">{% if user.is_admin %}Ja{% else %}Nee{% endif %}</td>
<td class="text-center px-6 py-4">
{% if not user.is_admin %}
<a href="{{ url_for('admin_impersonate', user_id=user.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Impersonate</a>
{% else %}-{% endif %}
</td>
<td class="text-center px-6 py-4">
{% if not user.is_admin %}
<form action="{{ url_for('admin_delete_user', user_id=user.id) }}" method="post" style="display:inline;" onsubmit="return confirm('Weet je zeker dat je deze gebruiker en alle bijbehorende kerkdata wilt verwijderen?');">
<button type="submit" class="bg-red-600 text-white text-xs px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Verwijderen</button>
</form>
{% else %}-{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('admin_dashboard') }}" class="bg-white text-black border border-black px-6 py-3 rounded-md font-semibold shadow hover:bg-gray-100 transition inline-block mt-6">Terug naar dashboard</a>
</div>
{% endblock %}

151
templates/base.html Normal file
View File

@@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}PsalmbordOnline{% endblock %}</title>
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.ico') }}">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Lora&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400..700;1,400..700&family=Quicksand:wght@300..700&display=swap" rel="stylesheet">
<!-- Tailwind CSS CDN -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style>
h1, h2, h3, h4, h5, h6 {
font-family: 'Lora', serif;
font-weight: 600;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: 'Quicksand';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Floating scrollbar style for WebKit browsers */
overflow-y: scroll;
}
/* Webkit Floating Scrollbar */
body::-webkit-scrollbar {
width: 8px;
background: transparent;
position: fixed;
}
body::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.3);
border-radius: 4px;
border: 2px solid transparent;
background-clip: content-box;
}
/* Firefox Floating scrollbar */
@-moz-document url-prefix() {
html {
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.3) transparent;
}
}
</style>
</head>
<body class="bg-white text-gray-900 font-sans min-h-screen flex flex-col">
<nav class="bg-white border-b border-gray-200 shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-20">
<!-- Logo -->
<div class="flex-shrink-0 flex items-center w-24">
{% if current_user.is_authenticated and current_user.church and current_user.church.logo_filename %}
<a href="{{ url_for('index') }}"><img src="{{ url_for('uploaded_file', filename=current_user.church.logo_filename) }}" alt="Logo" class="h-10 w-auto object-contain"></a>
{% else %}
<a href="{{ url_for('index') }}"><img src="{{ url_for('static', filename='default_logo.png') }}" alt="Default Logo" class="h-10 w-auto object-contain"></a>
{% endif %}
</div>
<!-- Desktop Nav -->
<div class="hidden sm:flex flex-1 justify-center">
<div id="nav-links" class="flex flex-row gap-2 items-center">
{% if current_user.is_authenticated %}
<a href="/portal" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Mijn schermen</a>
<a href="{{ url_for('global_schedules') }}" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Geplande diensten</a>
<a href="{{ url_for('church_settings') }}" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Instellingen</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin_dashboard') }}" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Admin</a>
{% endif %}
<a href="/logout" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Uitloggen</a>
{% else %}
<a href="/login" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Inloggen</a>
<a href="/register" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Registreren</a>
{% endif %}
</div>
</div>
<!-- Current Time -->
<div id="current-time" class="text-gray-700 font-semibold text-right text-sm w-24" title="Huidige datum en tijd">
<div id="current-date"></div>
<div id="current-clock" class="mt-0"></div>
</div>
<!-- Mobile hamburger -->
<div class="flex sm:hidden">
<button id="menu-button" class="p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-600" aria-label="Open menu">
<svg class="h-7 w-7 text-gray-700" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
<!-- Mobile nav menu -->
<div id="nav-mobile" class="sm:hidden hidden">
<div class="flex flex-col gap-2 py-3 items-center border-t border-gray-100">
{% if current_user.is_authenticated %}
<a href="/portal" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Mijn schermen</a>
<a href="{{ url_for('global_schedules') }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Geplande diensten</a>
<a href="{{ url_for('church_settings') }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Kerk instellingen</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin_dashboard') }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Admin instellingen</a>
{% endif %}
<a href="/logout" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Logout</a>
{% else %}
<a href="/login" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Login</a>
<a href="/register" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Register</a>
{% endif %}
</div>
</div>
</div>
</nav>
<script>
const menuButton = document.getElementById('menu-button');
const navMobile = document.getElementById('nav-mobile');
menuButton.addEventListener('click', () => {
navMobile.classList.toggle('hidden');
});
function updateTime() {
const now = new Date();
const dateOptions = { year: 'numeric', month: 'short', day: 'numeric' };
const timeOptions = { hour: '2-digit', minute: '2-digit', second: '2-digit' };
const dateStr = now.toLocaleDateString('nl-NL', dateOptions);
const timeStr = now.toLocaleTimeString('nl-NL', timeOptions);
document.getElementById('current-date').textContent = dateStr;
document.getElementById('current-clock').textContent = timeStr;
}
setInterval(updateTime, 1000);
updateTime();
</script>
<main class="flex-grow max-w-7xl mx-auto w-full px-6 py-8 sm:py-12">
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="bg-blue-100 border border-blue-400 text-blue-700 px-6 py-4 rounded mb-6">
{% for message in messages %}
<div>{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer class="bg-gray-100 text-gray-600 text-center py-4 text-sm">
&copy; {{ current_year }} PsalmbordOnline &nbsp;|&nbsp; Contact: <a href="mailto:info@psalmbord.online" class="text-blue-600 hover:underline">info@psalmbord.online</a>
</footer>
</body>
</html>

View File

@@ -0,0 +1,48 @@
{% extends "base.html" %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<h1 class="text-3xl font-semibold mb-6">Details voor {{ board.name }}</h1>
<!-- You can add more board settings above here as needed -->
<h2 class="text-2xl font-semibold mb-4">Schedules for this display</h2>
<div class="overflow-x-auto rounded-lg border border-gray-300">
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Days</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Start Time</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">End Time</th>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Content</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for s in schedules %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">{{ s.days_of_week }}</td>
<td class="text-center px-6 py-4 text-sm text-gray-900">{{ s.start_time.strftime('%H:%M') }}</td>
<td class="text-center px-6 py-4 text-sm text-gray-900">{{ s.end_time.strftime('%H:%M') }}</td>
<td class="px-6 py-4 text-sm text-gray-900">{{ s.content[:50] }}{% if s.content|length > 50 %}...{% endif %}</td>
<td class="text-center px-6 py-4">
<a href="{{ url_for('edit_schedule', board_id=board.id, schedule_id=s.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Edit</a>
<form method="post" action="{{ url_for('delete_schedule', board_id=board.id, schedule_id=s.id) }}" style="display:inline;">
<button type="submit" class="bg-red-600 text-white text-xs px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('add_schedule', board_id=board.id) }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition inline-block">Add Schedule</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<h1 class="text-3xl font-semibold mb-6">Manage Schedules for {{ board.name }}</h1>
<div class="overflow-x-auto rounded-lg border border-gray-300">
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Naam</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700">Datum</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Starttijd</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Eindtijd</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Acties</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for s in schedules %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">{{ s.name }}</td>
<td class="px-6 py-4 text-sm text-gray-900">{{ s.date.strftime('%Y-%m-%d') }}</td>
<td class="text-center px-6 py-4 text-sm text-gray-900">{{ s.start_time.strftime('%H:%M') }}</td>
<td class="text-center px-6 py-4 text-sm text-gray-900">{{ s.end_time.strftime('%H:%M') }}</td>
<td class="text-center px-6 py-4">
<a href="{{ url_for('edit_schedule', board_id=board.id, schedule_id=s.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Edit</a>
<form action="{{ url_for('delete_schedule', board_id=board.id, schedule_id=s.id) }}" method="POST" style="display:inline;">
<button type="submit" class="bg-red-600 text-white text-xs px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('add_schedule', board_id=board.id) }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition inline-block">Add Schedule</a>
<a href="{{ url_for('edit_board', board_id=board.id) }}" class="bg-white text-black border border-black px-6 py-3 rounded-md font-semibold shadow hover:bg-gray-100 transition inline-block ml-4">Terug naar bord bewerken</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,36 @@
{% extends 'base.html' %}
{% block title %}Wachtwoord wijzigen{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<div class="max-w-md mx-auto">
<h2 class="text-3xl font-semibold mb-6">Wijzig Wachtwoord</h2>
{% if msg %}
<div class="bg-blue-100 border border-blue-400 text-blue-700 px-6 py-4 rounded mb-6">{{ msg }}</div>
{% endif %}
<form method="post" class="space-y-6">
<div>
<label for="current_password" class="block mb-3 font-semibold text-gray-700">Huidig wachtwoord</label>
<input type="password" id="current_password" name="current_password" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="new_password" class="block mb-3 font-semibold text-gray-700">Nieuw wachtwoord</label>
<input type="password" id="new_password" name="new_password" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="confirm_password" class="block mb-3 font-semibold text-gray-700">Bevestig nieuw wachtwoord</label>
<input type="password" id="confirm_password" name="confirm_password" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<button type="submit" class="bg-[#f7d91a] text-black w-full rounded-md py-4 font-semibold shadow hover:bg-yellow-300 transition">Opslaan</button>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,104 @@
{% extends 'base.html' %}
{% block title %}Kerk Instellingen{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
{% if msg %}
<div class="bg-blue-100 border border-blue-400 text-blue-700 px-6 py-4 rounded mb-6">{{ msg }}</div>
{% endif %}
<div class="flex flex-wrap gap-10">
<form method="post" class="flex-1 min-w-[300px] max-w-lg space-y-6" enctype="multipart/form-data">
<input type="hidden" name="form_type" value="contact">
<h3 class="text-2xl font-semibold">Kerkgegevens</h3>
<div>
<label for="name" class="block mb-3 font-semibold text-gray-700">Kerknaam</label>
<input type="text" id="name" name="name" value="{{ church.name }}" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="contact_email" class="block mb-3 font-semibold text-gray-700">Contact Email</label>
<input type="email" id="contact_email" name="contact_email" value="{{ church.contact_email }}" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="contact_phone" class="block mb-3 font-semibold text-gray-700">Telefoon</label>
<input type="text" id="contact_phone" name="contact_phone" value="{{ church.contact_phone }}" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="contact_address" class="block mb-3 font-semibold text-gray-700">Adres</label>
<input type="text" id="contact_address" name="contact_address" value="{{ church.contact_address }}" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="logo" class="block mb-3 font-semibold text-gray-700">Logo uploaden</label>
<input type="file" id="logo" name="logo" accept="image/png, image/jpeg, image/jpg, image/gif" class="w-full border border-gray-300 rounded-md p-4">
</div>
<button type="submit" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition">Opslaan</button>
</form>
<div class="flex-1 min-w-[300px] max-w-md">
<h3 class="text-2xl font-semibold mb-4">Gebruikers van deze kerk</h3>
<div class="overflow-x-auto rounded-lg border border-gray-300 mb-6">
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Gebruikersnaam</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for user in users %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900 flex items-center justify-between">
{{ user.username }}
{% if user.id != current_user.id %}
<form method="post" action="{{ url_for('church_delete_user', user_id=user.id) }}" onsubmit="return confirm('Weet je zeker dat je deze gebruiker wilt verwijderen?');">
<button type="submit" class="bg-red-600 text-white px-6 py-3 rounded font-semibold hover:bg-red-700 ml-4 transition">Verwijderen</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<h3 class="text-2xl font-semibold mb-4">Nieuwe gebruiker toevoegen aan deze kerk</h3>
<form method="post" class="space-y-6" >
<input type="hidden" name="form_type" value="user">
<div>
<label for="username" class="block mb-3 font-semibold text-gray-700">Emailadres</label>
<input type="email" id="username" name="username" placeholder="email@voorbeeld.com" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="password" class="block mb-3 font-semibold text-gray-700">Wachtwoord</label>
<input type="password" id="password" name="password" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<button type="submit" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition">Toevoegen</button>
</form>
<a href="{{ url_for('change_password') }}" class="bg-white text-black border border-black px-4 py-2 rounded-md font-semibold shadow hover:bg-gray-100 transition inline-block mt-6 inline-block text-base">Wachtwoord wijzigen</a>
</div>
</div>
<a href="{{ url_for('portal') }}" class="bg-white text-black border border-black px-6 py-3 rounded-md font-semibold shadow hover:bg-gray-100 transition inline-block">Terug naar portal</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,228 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Liturgiebord</title>
<style>
html, body {
height: 100%;
margin: 0;
padding: 0;
}
body {
background: #222;
color: #fff;
font-family: 'Lora', serif;
height: 1920px;
width: 1080px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.liturgie-board {
width: 1080px;
height: 1920px;
display: flex;
flex-direction: column;
justify-content: flex-start; /* Align all rows from the top */
align-items: flex-start;
gap: 0vh;
padding-left: 0;
position: relative;
}
.liturgie-line {
margin: auto;
width: 1080px;
justify-items: center;
text-align: left;
font-size: 130px;
line-height: 1.1;
white-space: nowrap;
height: 190px; /* Fixed height ensures every row occupies space, even when empty */
box-sizing: border-box;
margin: 0;
padding: 20px;
z-index: 2;
text-shadow: 2px 2px 8px #000, 0 0 4px #000;
/* Optional: Uncomment for visible empty row borders */
/* border-bottom: 1px solid rgba(255,255,255,0.15); */
}
.liturgie-bg-overlay {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.3);
z-index: 1;
pointer-events: none;
}
</style>
</head>
<body>
<div style="transform-origin: top left;">
<div class="liturgie-board" {% if board.background_image %}
style="background-image: url('{% if board.background_image.startswith('default_') %}{{ url_for('static', filename=board.background_image) }}{% else %}{{ url_for('uploaded_file', filename=board.background_image) }}{% endif %}'); background-size: cover; background-position: center; background-repeat: no-repeat;"
{% endif %}>
{% if board.background_image %}<div class="liturgie-bg-overlay"></div>{% endif %}
{% if schedule_content %}
{% set lines = schedule_content.split('\n') %}
{% for i in range(10) %}
<div class="liturgie-line">{{ lines[i] if lines|length > i else ' ' }}</div>
{% endfor %}
{% else %}
<div class="liturgie-line">{{ board.line1 or ' ' }}</div>
<div class="liturgie-line">{{ board.line2 or ' ' }}</div>
<div class="liturgie-line">{{ board.line3 or ' ' }}</div>
<div class="liturgie-line">{{ board.line4 or ' ' }}</div>
<div class="liturgie-line">{{ board.line5 or ' ' }}</div>
<div class="liturgie-line">{{ board.line6 or ' ' }}</div>
<div class="liturgie-line">{{ board.line7 or ' ' }}</div>
<div class="liturgie-line">{{ board.line8 or ' ' }}</div>
<div class="liturgie-line">{{ board.line9 or ' ' }}</div>
<div class="liturgie-line">{{ board.line10 or ' ' }}</div>
{% endif %}
</div>
</div>
<script>
// LIVE PREVIEW FEATURE: Accept messages with new data and update the lines/background
window.addEventListener('message', function(event) {
// You may want to restrict this for security:
// if (event.origin !== 'YOUR_DOMAIN') return;
var data = event.data;
if (!data || typeof data !== 'object' || !data.type || data.type !== 'updateBoard') return;
// Update all lines
for (let i = 1; i <= 10; i++) {
var el = document.querySelector('.liturgie-line:nth-child(' + (data.bg ? i+1 : i) + ')');
if (el) el.textContent = (data['line'+i] || ' ');
}
// Update background if sent
if ('bg' in data) {
document.querySelector('.liturgie-board').style.backgroundImage = data.bg ? ("url('" + data.bg + "')") : 'none';
// Show/hide overlay
var overlay = document.querySelector('.liturgie-bg-overlay');
if (overlay) {
overlay.style.display = data.bg ? '' : 'none';
}
}
}, false);
</script>
<script>
// Poll every 5 seconds to check for active schedule update
(function() {
let currentLines = [];
let currentBackground = null;
{% if schedule_content %}
currentLines = {{ schedule_content.split('\n')|tojson }};
{% else %}
currentLines = Array(10).fill(' ');
{% endif %}
const boardId = {{ board.id }};
function updateBoardFromData(data) {
// Use postMessage event format to update lines & background
const msg = { type: 'updateBoard' };
for (let i = 1; i <= 10; i++) {
msg['line'+i] = data.lines[i-1] || ' ';
}
if (data.background) {
msg.bg = data.background;
} else {
msg.bg = null;
}
window.postMessage(msg, '*');
currentLines = data.lines;
currentBackground = data.background || null;
}
async function pollActiveSchedule() {
try {
const response = await fetch(`/board/${boardId}/active_schedule_json`);
if (!response.ok) throw new Error('Network response not ok');
const data = await response.json();
// Compare new lines with current lines
let changed = false;
for (let i = 0; i < 10; i++) {
if ((data.lines[i] || ' ') !== (currentLines[i] || ' ')) {
changed = true;
break;
}
}
// Check for background change
if ((data.background || null) !== currentBackground) {
changed = true;
}
if (changed) {
updateBoardFromData(data);
}
} catch (e) {
console.error('Error polling active schedule:', e);
}
}
setInterval(pollActiveSchedule, 5000);
})();
</script>
<script>
// Poll every 5 seconds to check for active schedule update
(function() {
let currentLines = [];
{% if schedule_content %}
currentLines = {{ schedule_content.split('\n')|tojson }};
{% else %}
currentLines = Array(10).fill(' ');
{% endif %}
const boardId = {{ board.id }};
function updateBoardFromData(data) {
// Use postMessage event format to update lines & background
const msg = { type: 'updateBoard' };
for (let i = 1; i <= 10; i++) {
msg['line'+i] = data.lines[i-1] || ' ';
}
if (data.background) {
msg.bg = data.background;
}
window.postMessage(msg, '*');
currentLines = data.lines;
}
async function pollActiveSchedule() {
try {
const response = await fetch(`/board/${boardId}/active_schedule_json`);
if (!response.ok) throw new Error('Network response not ok');
const data = await response.json();
// Compare new lines with current lines
let changed = false;
for (let i = 0; i < 10; i++) {
if ((data.lines[i] || ' ') !== (currentLines[i] || ' ')) {
changed = true;
break;
}
}
// Also consider background change
// We can extend this if needed, skipping for now
if (changed) {
updateBoardFromData(data);
}
} catch (e) {
console.error('Error polling active schedule:', e);
}
}
setInterval(pollActiveSchedule, 5000);
})();
</script>
</body>
</html>

399
templates/edit_board.html Normal file
View File

@@ -0,0 +1,399 @@
{% extends 'base.html' %}
{% block title %}Bewerk Liturgiebord - Digitale Liturgie{% endblock %}
{% block content %}
<style>
.edit-board-container {
padding: 1rem 1rem;
background: white;
border-radius: 0.5rem;
margin: 0 auto 1rem auto;
}
.board-title {
font-size: 2rem;
font-weight: 600;
margin-bottom: 1rem;
color: #000000;
text-align: left;
}
.lines-section {
margin-bottom: 2.5rem;
}
.line-input-group {
margin-bottom: 1.25rem;
}
.line-input-group label {
display: block;
margin-bottom: 0.75rem;
font-weight: 700;
color: #374151;
}
.line-input-group input {
width: 100%;
padding: 0.75rem 1rem;
font-size: 1.125rem;
border-radius: 0.5rem;
border: 1px solid #d1d5db;
box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.line-input-group input:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 5px #93c5fd;
}
.button-group {
display: flex;
gap: 1rem;
justify-content: flex-start;
margin-top: 1.5rem;
}
.button-group button {
padding: 0.75rem 1.75rem;
font-size: 1rem;
border-radius: 0.5rem;
font-weight: 700;
transition: background-color 0.15s ease-in-out;
}
.schedule-section {
max-width: 100%;
margin-bottom: 2.5rem;
}
.schedule-section h3 {
margin-bottom: 1rem;
}
.schedule-section table {
width: 100%;
}
.preview-section {
max-width: 100%;
text-align: center;
font-family: 'Lora', serif;
}
.preview-wrapper {
width: 250px;
height: 444px; /* 9:16 aspect ratio */
margin: 0 auto;
background: #222;
border-radius: 0.5rem;
overflow: hidden;
}
iframe#live-preview-iframe {
width: 1080px;
height: 1920px;
border: 0;
pointer-events: none;
transform-origin: top left;
transform: scale(0.23);
margin: 0;
display: block;
}
@media (max-width: 767px) {
.edit-board-container {
padding: 15px;
margin-bottom: 1rem;
border-radius: 0.5rem;
}
.board-title {
font-size: 1.75rem;
margin-bottom: 1.5rem;
}
.button-group {
flex-direction: column;
gap: 8px;
}
.button-group button {
width: 100%;
}
.preview-wrapper {
width: 180px;
height: 320px;
margin: 0 auto;
border-radius: 0.5rem;
background: #222;
overflow: hidden;
}
iframe#live-preview-iframe {
transform: scale(0.23);
position: static;
width: 1080px;
height: 1920px;
margin: 0 auto;
display: block;
}
}
/* Highlight for drag-over between lines in preview */
.liturgie-line.line-over {
background: rgba(255,255,255,0.22);
border: 2px dashed #fff;
box-sizing: border-box;
outline: none;
transition: background 0.12s, border 0.12s;
}
.liturgie-line.swap-animate {
animation: swap-pulse 0.35s;
}
@keyframes swap-pulse {
0% { background: #ffe066; }
60% { background: #fff3cd; }
100% { background: none; }
}
</style>
<div class="edit-board-container">
<div style="display: flex; gap: 2rem; align-items: flex-start; flex-wrap: wrap;">
<!-- Left: Live (editable) preview -->
<form method="post" id="boardForm" style="flex: 1 1 300px; min-width: 250px; max-width:250px;">
<div class="preview-section">
<div class="preview-wrapper"
style="width:250px; height:444px; background: #222; border-radius: 0.5rem; overflow: hidden; position:relative;
background-image: {% if board.background_image %}url('{% if board.background_image.startswith('default_') %}{{ url_for('static', filename=board.background_image) }}{% else %}{{ url_for('uploaded_file', filename=board.background_image) }}{% endif %}'){% else %}none{% endif %}; background-size:cover; background-position:center;">
{% if board.background_image %}
<div class="liturgie-bg-overlay" style="
content:''; position:absolute; top:0; left:0; right:0; bottom:0;
background:rgba(0,0,0,0.3); z-index:1; pointer-events:none;">
</div>
{% endif %}
{% if schedule_active %}
{% set matched_schedules = schedules|selectattr('content', 'equalto', schedule_content)|list %}
<div style="position:absolute; top:0; left:0; right:0; bottom:0; z-index:10; display:flex; justify-content:center; align-items:center; background: rgba(0,0,0,0.7); color:white; font-weight:bold; font-size:1rem; text-align:center; padding: 1rem;">
Geplande dienst ({{ matched_schedules[0].name if matched_schedules|length > 0 else 'Onbekend' }}) is nu actief
</div>
{% endif %}
<div class="liturgie-board"
style="width:100%; height:100%; display:flex; flex-direction:column; justify-content:center; align-items:flex-start; gap:0.6vh; padding-left:5%; position:relative; z-index:2;">
{% if schedule_active %}
{% set lines = schedule_content.split('\\n') %}
{% for i in range(10) %}
<div class="liturgie-line" style="width:90%; font-size:28px; line-height:1.2; text-shadow:2px 2px 8px #000, 0 0 4px #000; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin:0; padding:0; color:#fff; text-align:left;">
{{ lines[i] if lines|length > i else ' ' }}
</div>
{% endfor %}
{% else %}
{% for i in range(1, 11) %}
<div class="liturgie-line"
contenteditable="true"
data-line="line{{ i }}"
draggable="true"
style="width:90%; font-size:28px; line-height:1.2; text-shadow:2px 2px 8px #000, 0 0 4px #000; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin:0; padding:0; color:#fff; text-align:left;"
>{{ board['line' ~ i] or ' ' }}</div>
{% endfor %}
{% endif %}
</div>
</div>
{% if schedule_active %}
<p style="color: red;">Er is een dienst actief! Pas deze aan via de planning.</p>
{% endif %}
</div>
</form>
<!-- Right: Schedule section -->
<div class="schedule-section" style="flex: 1 1 380px; min-width:330px;">
<form method="post" style="margin-bottom: 1rem;">
<input type="hidden" name="form_type" value="name">
<div style="display:flex; gap:8px; align-items:center;">
<input id="boardNameInput" name="name" type="text" value="{{ board.name }}" style="font-size:1.1rem; padding:0.3em 0.6em; border-radius:0.3em; border:1px solid #aaa; width:60%;">
<button type="submit" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded hover:bg-yellow-300 font-semibold transition" style="margin-left:5px;">Opslaan</button>
</div>
</form>
<h3 class="text-xl font-semibold mb-3">Geplande diensten</h3>
<div class="overflow-x-auto rounded-lg border border-gray-300">
{% if schedules|length == 0 %}
<p class="p-4">Geen geplande inhoud.</p>
{% else %}
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Naam</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Datum</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Starttijd</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Eindtijd</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Acties</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for s in schedules %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-gray-900 flex items-center gap-2">
<span style="display:inline-block; width:0.75rem; height:0.75rem; border-radius:9999px;"
title="{% if schedule_active and active_schedule and s.id == active_schedule.id %}Actief{% else %}Niet actief{% endif %}"
class="{% if schedule_active and active_schedule and s.id == active_schedule.id %}bg-green-500{% else %}bg-red-500{% endif %}">
</span>
{{ s.name }}
</td>
<td class="px-6 py-4 text-center text-gray-900">{{ s.date.strftime('%Y-%m-%d') }}</td>
<td class="px-6 py-4 text-center text-gray-900">{{ s.start_time.strftime('%H:%M') }}</td>
<td class="px-6 py-4 text-center text-gray-900">{{ s.end_time.strftime('%H:%M') }}</td>
<td class="px-6 py-4 text-center">
<!-- Removed delete button here: schedules can't be deleted from board edit page -->
<a href="{{ url_for('edit_global_schedule', schedule_id=s.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded hover:bg-yellow-300 font-semibold transition">Bewerken</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
<a href="{{ url_for('add_global_schedule') }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded hover:bg-yellow-300 font-semibold inline-block mt-3 transition">Dienst toevoegen</a>
<div class="button-group mt-3">
<button type="button" class="bg-white text-black border border-black px-6 py-3 rounded font-semibold hover:bg-gray-100 transition"
id="save-and-upload-bg"
data-edit-url="{{ url_for('edit_board', board_id=board.id) }}"
data-upload-url="{{ url_for('upload_background', board_id=board.id) }}">
Achtergrondafbeelding wijzigen
</button>
<!-- Removed Opslaan button as live save is implemented -->
</div>
</div>
</div>
</div>
{% if not schedule_active %}
<script>
// --- Line drag-and-swap logic ---
document.addEventListener('DOMContentLoaded', function () {
let dragSrc = null;
const lines = document.querySelectorAll('.liturgie-line[data-line]');
lines.forEach(line => {
line.addEventListener('dragstart', function (e) {
dragSrc = this;
this.classList.add('dragging');
});
line.addEventListener('dragend', function (e) {
this.classList.remove('dragging');
dragSrc = null;
lines.forEach(l => l.classList.remove('line-over'));
});
line.addEventListener('dragover', function (e) {
e.preventDefault();
if (this !== dragSrc) this.classList.add('line-over');
});
line.addEventListener('dragleave', function (e) {
this.classList.remove('line-over');
});
line.addEventListener('drop', function (e) {
e.preventDefault();
this.classList.remove('line-over');
if (dragSrc && dragSrc !== this) {
// Only swap textContent, not the whole element
const tmp = this.textContent;
this.textContent = dragSrc.textContent;
dragSrc.textContent = tmp;
// animation for both lines
this.classList.add('swap-animate');
dragSrc.classList.add('swap-animate');
setTimeout(() => {
this.classList.remove('swap-animate');
dragSrc.classList.remove('swap-animate');
}, 400);
// trigger input event for autosave
this.dispatchEvent(new Event('input'));
dragSrc.dispatchEvent(new Event('input'));
}
});
});
});
// --- End drag-and-swap logic ---
// block enter key
document.getElementById('boardForm').addEventListener('keydown', function (event) {
if (event.key === 'Enter') {
event.preventDefault();
}
});
// end of block enter key
// On submit, copy preview lines to hidden fields
const boardForm = document.getElementById('boardForm');
if (boardForm) {
for (let i = 1; i <= 10; i++) {
// create hidden inputs for each line
let hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = 'line'+i;
hidden.id = 'hidden-line'+i;
boardForm.appendChild(hidden);
}
async function saveLines() {
const formData = new FormData();
for (let i = 1; i <= 10; i++) {
const text = document.querySelector('.liturgie-line[data-line="line'+i+'"]').textContent;
formData.append('line'+i, text);
}
const response = await fetch(boardForm.action, {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
if (!response.ok) {
console.error('Failed to save lines.');
}
}
// Add auto save event listeners to editable lines with debounce
for (let i = 1; i <= 10; i++) {
const editableLine = document.querySelector('.liturgie-line[data-line="line'+i+'"]');
if (editableLine) {
editableLine.addEventListener('input', () => {
if (window.autoSaveTimeout) clearTimeout(window.autoSaveTimeout);
window.autoSaveTimeout = setTimeout(saveLines, 1000);
});
}
}
// Prevent form's default submit to avoid page reload on autosave
boardForm.addEventListener('submit', function(e) {
if (document.getElementById('redirect_to_upload').value === '0') {
e.preventDefault();
saveLines();
}
});
}
// Added script for wallpaper button
document.addEventListener('DOMContentLoaded', function() {
const btn = document.getElementById('save-and-upload-bg');
if (btn) {
btn.addEventListener('click', function() {
const uploadUrl = this.getAttribute('data-upload-url');
if (uploadUrl) {
window.location.href = uploadUrl;
}
});
}
});
</script>
{% else %}
<script>
// When schedule is active, there's nothing to submit for the lines and nothing to sync.
</script>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,43 @@
{% extends 'base.html' %}
{% block title %}Alle Planningen{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<h1 class="text-3xl font-semibold mb-6">Alle planningen</h1>
<div class="overflow-x-auto rounded-lg border border-gray-300">
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Naam</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Datum</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Starttijd</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Eindtijd</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Toegewezen borden</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Acties</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for schedule in schedules %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-gray-900">{{ schedule.name }}</td>
<td class="px-6 py-4 text-center text-gray-900">{{ schedule.date.strftime('%Y-%m-%d') }}</td>
<td class="px-6 py-4 text-center text-gray-900">{{ schedule.start_time.strftime('%H:%M') }}</td>
<td class="px-6 py-4 text-center text-gray-900">{{ schedule.end_time.strftime('%H:%M') }}</td>
<td class="px-6 py-4 text-center text-gray-900">
{% for board in schedule.boards %}{{ board.name }}{% if not loop.last %}, {% endif %}{% endfor %}
</td>
<td class="px-6 py-4 text-center">
<a href="{{ url_for('edit_global_schedule', schedule_id=schedule.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Bewerken</a>
<form action="{{ url_for('delete_global_schedule', schedule_id=schedule.id) }}" method="POST" style="display:inline;" onsubmit="return confirm('Weet je zeker dat je deze planning wilt verwijderen?');">
<button type="submit" class="bg-red-600 text-white text-xs px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Verwijderen</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('add_global_schedule') }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 inline-block mb-6 transition">Nieuwe planning toevoegen</a>
</div>
{% endblock %}

337
templates/index.html Normal file
View File

@@ -0,0 +1,337 @@
{% extends "base.html" %}
{% block title %}Welkom bij Digitale Liturgie{% endblock %}
{% block content %}
<style>
/* Modern Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
body { font-family: 'Inter', sans-serif; }
.hero-section {
position: relative;
background: url("{{ url_for('static', filename='Hero.jpg') }}") no-repeat center center/cover;
min-height: 30rem;
border-radius: 1.15rem;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
animation: fadeInHero 1.2s cubic-bezier(.77,0,.18,1) both;
}
@keyframes fadeInHero {
from { opacity: 0; transform: translateY(-30px);}
to { opacity: 1; transform: translateY(0);}
}
.hero-overlay {
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(40,40,50,0.75) 0%, rgba(10,0,40,0.40) 70%);
z-index: 1;
border-radius: 1.15rem;
}
.hero-content {
position: relative;
z-index: 2;
max-width: 42rem;
text-align: center;
padding: 2rem 2rem;
animation: fadeUp 1.2s .3s cubic-bezier(.77,0,.18,1) both;
}
.hero-content h1 {
font-size: 3rem;
font-weight: 700;
text-shadow: 0 5px 32px rgba(0,0,0,0.32);
margin-bottom: 0.5rem;
letter-spacing: -1.2px;
line-height: 1.1;
}
.hero-content p {
font-size: 1.25rem;
margin-bottom: 2rem;
color: #f4f4f4;
}
.modern-btn {
background: linear-gradient(90deg, #f7d91a 0%, #ffe486 100%);
color: #181818;
font-weight: 600;
border: none;
padding: 1rem 2.3rem;
border-radius: 0.66rem;
font-size: 1.14rem;
box-shadow: 0 4px 24px rgba(247,217,26,0.13);
transition: box-shadow 0.23s, background 0.23s, transform 0.18s;
cursor: pointer;
}
.modern-btn:hover {
background: linear-gradient(90deg,#ffe486 0%, #f7d91a 100%);
box-shadow: 0 8px 28px rgba(247,217,26,0.19);
transform: translateY(-2px) scale(1.045);
color: #25201b;
}
/* Contact section styling */
.contact-section {
background: white;
border-radius: 1.15rem;
margin-top: 1rem;
padding: 2.2rem 1.5rem;
box-shadow: 0 8px 24px rgba(0,0,0,0.09);
max-width: 98vw;
min-width: 280px;
animation: fadeUp 1.1s .6s cubic-bezier(.77,0,.18,1) both;
}
.contact-section h2 {
font-size: 1.8rem;
font-weight: 600;
margin-bottom: 1.5rem;
}
.contact-section input,
.contact-section textarea {
width: 100%;
border: 1.3px solid #e6e6e6;
border-radius: 0.5rem;
padding: 0.9rem 1.14rem;
font-size: 1rem;
margin-bottom: 1rem;
background: #f8f9fa;
transition: border-color 0.2s;
}
.contact-section input:focus,
.contact-section textarea:focus {
border-color: #f7d91a;
outline: none;
background: #fffde6;
}
.contact-section button {
margin-top: 0.3rem;
width: 100%;
}
/* Cards Area */
.features-area {
display: grid;
gap: 2.2rem;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
max-width: 1200px;
margin: 0 auto;
margin-top: 2.2rem;
margin-bottom: 2.5rem;
animation: fadeInStagger 1s cubic-bezier(.77,0,.18,1) both;
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(50px);}
to { opacity: 1; transform: translateY(0);}
}
@keyframes fadeInStagger {
from { opacity: 0;}
to { opacity: 1;}
}
.feature-card {
background: white;
border-radius: 1.09rem;
box-shadow: 0 6px 20px rgba(0,0,0,0.06);
padding: 2rem 1.2rem 1.4rem 1.2rem;
text-align: center;
min-height: 19.5rem;
animation: fadeUp 0.8s cubic-bezier(.77,0,.18,1) both;
transition: transform 0.22s, box-shadow 0.22s;
will-change: transform;
/* Animation delay stagger per card (manual in HTML) */
}
.feature-card:hover {
transform: translateY(-7px) scale(1.02);
box-shadow: 0 16px 40px rgba(247,217,26,0.13);
}
.feature-card img {
max-width: 55px;
margin-bottom: 1.1rem;
margin-top: -0.8rem;
filter: drop-shadow(0 0 2px rgba(40,40,40,0.06));
transition: transform 0.17s;
}
.feature-card:hover img {
transform: scale(1.095) rotate(2deg);
}
.feature-card h3 {
font-size: 1.23rem;
margin-bottom: 0.9rem;
font-weight: 700;
color: #181a1d;
letter-spacing: -0.02em;
}
.feature-card ul {
text-align: left;
color: #434243;
font-size: 1.02rem;
list-style: none;
padding-left: 0.7em;
line-height: 1.7;
}
.feature-card ul li::before {
content: '• ';
color: #f7d91a;
font-weight: bold;
}
/* Responsive hero/contact split */
@media (min-width: 1000px) {
.hero-container {
display: flex;
gap: 2.4rem;
max-width: 1330px;
margin: 0 auto 3.3rem auto;
}
.hero-section, .contact-section {
flex: 1.2;
min-height: 32rem;
}
.contact-section {
min-width: 350px;
margin-top: 0;
max-width: 400px;
}
}
@media (max-width: 780px) {
.hero-section {
min-height: 22rem;
}
.hero-content h1 { font-size: 2rem;}
.features-area { gap: 1rem;}
.feature-card { padding: 1.3rem 0.5rem; }
}
</style>
<div class="hero-section" style="margin-bottom:2.5rem;">
<div class="hero-overlay"></div>
<div class="hero-content">
<h1>Psalmbord Online</h1>
<p>
Welkom bij Psalmbord Online.<br>
Beheer, plan en presenteer moeiteloos je kerkdiensten.<br>
Start direct met een eigen account!
</p>
<a href="/login" class="modern-btn">Log in</a>
</div>
</div>
<div class="section-intro" style="margin-top: 3.7rem; margin-bottom:1.5rem; animation: fadeUp 1.1s .3s cubic-bezier(.77,0,.18,1) both;">
<h2 class="text-3xl font-bold text-center mb-2" style="font-size:2.1rem;">Wat kan Digitale Liturgie voor u doen?</h2>
<p class="text-center text-md mb-8" style="color:#62666b;">Beheer en presenteer moeiteloos uw kerkdiensten, gebruikers en schermen.</p>
</div>
<div class="features-area">
<div class="feature-card" style="animation-delay:.09s;">
<img src="{{ url_for('static', filename='icons/group.png') }}" alt="Gebruikersbeheer">
<h3>Gebruikersbeheer</h3>
<ul>
<li>Aanmelden & Registreren</li>
<li>Wachtwoordbeheer</li>
<li>Lid & Admin-rollen</li>
</ul>
</div>
<div class="feature-card" style="animation-delay:.20s;">
<img src="{{ url_for('static', filename='icons/church.png') }}" alt="Kerkenbeheer">
<h3>Kerkenbeheer</h3>
<ul>
<li>Kerk aanmaken & aanpassen</li>
<li>Logo uploaden</li>
<li>Activatie beheren</li>
</ul>
</div>
<div class="feature-card" style="animation-delay:.35s;">
<img src="{{ url_for('static', filename='icons/board.png') }}" alt="Bordenbeheer">
<h3>Bordenbeheer</h3>
<ul>
<li>Borden toevoegen, bewerken, verwijderen</li>
<li>Max. 10 tekstregels per bord</li>
<li>Achtergrond instellen of uploaden</li>
</ul>
</div>
<div class="feature-card" style="animation-delay:.50s;">
<img src="{{ url_for('static', filename='icons/task.png') }}" alt="Planningen">
<h3>Planningen</h3>
<ul>
<li>Diensten & liturgie-invulling per datum</li>
<li>Meerdere borden per planning</li>
<li>Automatisch tonen volgens tijd</li>
</ul>
</div>
</div>
<!-- Contact form at BOTTOM -->
<div class="contact-section" style="max-width:none; margin:3.8rem 0 0 0; width:100%;">
<h2>Contacteer ons</h2>
<form method="POST" action="/contact" autocomplete="off">
<div class="contact-form-fields">
<div class="inputs-left">
<input type="text" id="name" name="name" placeholder="Naam" required>
<input type="text" id="church" name="church" placeholder="Kerknaam" required>
<input type="tel" id="phone" name="phone" placeholder="Telefoonnummer" required>
<input type="email" id="email" name="email" placeholder="E-mail" required>
</div>
<div class="textarea-right">
<textarea id="message" name="message" rows="7" placeholder="Uw bericht..." required></textarea>
</div>
</div>
<button type="submit" class="modern-btn">Verstuur</button>
</form>
<style>
.contact-form-fields {
display: flex;
gap: 2.2rem;
align-items: stretch;
}
.inputs-left {
flex: 1 1 210px;
display: flex;
flex-direction: column;
gap: 1rem;
min-width: 190px;
}
.inputs-left input {
opacity: 0;
transform: translateY(32px);
animation: fadeFormInput 0.8s cubic-bezier(.77,0,.18,1) forwards;
}
.inputs-left input:nth-child(1) { animation-delay: 0.18s; }
.inputs-left input:nth-child(2) { animation-delay: 0.32s; }
.inputs-left input:nth-child(3) { animation-delay: 0.46s; }
.inputs-left input:nth-child(4) { animation-delay: 0.6s; }
.textarea-right textarea {
opacity: 0;
transform: translateY(32px);
animation: fadeFormInput 0.8s 0.74s cubic-bezier(.77,0,.18,1) forwards;
height: 100%;
min-height: 150px;
resize: vertical;
}
.contact-section button.modern-btn {
opacity: 0;
transform: translateY(32px);
animation: fadeFormInput 0.8s 1s cubic-bezier(.77,0,.18,1) forwards;
}
@keyframes fadeFormInput {
from { opacity: 0; transform: translateY(32px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 800px) {
.contact-form-fields {
flex-direction: column;
gap: 0;
}
.textarea-right, .inputs-left {
min-width: 0;
}
.textarea-right textarea {
min-height: 100px;
}
}
</style>
</div>
{% endblock %}

27
templates/login.html Normal file
View File

@@ -0,0 +1,27 @@
{% extends 'base.html' %}
{% block title %}Login - Digitale Liturgie{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<div class="max-w-md mx-auto">
<h2 class="text-3xl font-semibold mb-6">Login</h2>
<form method="post" class="space-y-6">
<div>
<label for="username" class="block mb-3 font-semibold text-gray-700">Gebruikersnaam</label>
<input type="text" id="username" name="username" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="password" class="block mb-3 font-semibold text-gray-700">Wachtwoord</label>
<input type="password" id="password" name="password" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<button type="submit" class="bg-[#f7d91a] text-black w-full rounded-md py-4 font-semibold shadow hover:bg-yellow-300 transition">Login</button>
</form>
</div>
</div>
{% endblock %}

106
templates/portal.html Normal file
View File

@@ -0,0 +1,106 @@
{% extends 'base.html' %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<h2 class="text-3xl font-semibold mb-6">Welkom, {{ current_user.username }}!</h2>
<div class="overflow-x-auto rounded-lg border border-gray-300">
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Naam</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700">URL</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Bewerken</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Weergeven</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Verwijderen</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for board in boards %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-gray-900">{{ board.name }}</td>
<td class="px-6 py-4 text-blue-600 break-words">
{% if board.church.is_active %}
<a href="{{ url_for('display_board_unique', unique_id=board.unique_id) }}" target="_blank" class="underline hover:text-blue-800 transition">display/{{ board.unique_id }}</a>
{% else %}
<em>Geen actieve licentie voor dit scherm</em>
{% endif %}
</td>
<td class="text-center px-6 py-4">
<a href="{{ url_for('edit_board', board_id=board.id) }}" class="bg-[#f7d91a] text-black text-xs px-2 py-1 rounded hover:bg-yellow-300 transition">Bewerken</a>
</td>
<td class="relative text-center px-6 py-4">
{% if board.church.is_active %}
<a href="{{ url_for('display_board_unique', unique_id=board.unique_id) }}" class="bg-black text-white text-xs px-2 py-1 rounded border border-black hover:bg-gray-800 transition show-iframe-preview" target="_blank" data-url="{{ url_for('display_board_unique', unique_id=board.unique_id) }}">Weergeven</a>
<div class="iframe-preview-popup" style="display:none; position:absolute; top:0; left:100%; margin-left:8px; width:400px; height:225px; border:1px solid #ccc; box-shadow:0 2px 8px rgba(0,0,0,0.2); z-index:50;">
<iframe src="" frameborder="0" style="width:100%; height:100%; transform-origin: top left; transform: scale(0.2);"></iframe>
</div>
{% endif %}
</td>
<td class="text-center px-6 py-4">
<form method="post" action="{{ url_for('delete_board', board_id=board.id) }}" style="display:inline;">
<button type="submit" class="bg-white text-black text-xs px-2 py-1 rounded border border-black hover:bg-gray-100 transition" onclick="return confirm('Weet je zeker dat je dit bord wilt verwijderen?');">Verwijderen</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('add_board') }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 inline-block mb-6 transition">Nieuw liturgiebord toevoegen</a>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const showIframeLinks = document.querySelectorAll('.show-iframe-preview');
// Create a single popup container to be reused
const popup = document.createElement('div');
popup.style.position = 'absolute';
popup.style.width = '1080px';
popup.style.height = '1920px';
popup.style.border = '1px solid #ccc';
popup.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
popup.style.zIndex = '9999';
popup.style.display = 'none';
popup.style.background = 'white';
popup.style.transformOrigin = 'top left';
popup.style.transform = 'scale(0.2)';
// iframe inside popup
const iframe = document.createElement('iframe');
iframe.frameBorder = 0;
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.transform = '';
iframe.style.transformOrigin = 'top left';
popup.appendChild(iframe);
document.body.appendChild(popup);
showIframeLinks.forEach(link => {
link.addEventListener('mouseenter', (event) => {
const url = link.getAttribute('data-url');
iframe.src = url;
const rect = link.getBoundingClientRect();
// Position popup to the right and vertically aligned with the link
popup.style.left = (window.scrollX + rect.right + 8) + 'px';
popup.style.top = (window.scrollY + rect.top) + 'px';
popup.style.display = 'block';
});
link.addEventListener('mouseleave', () => {
popup.style.display = 'none';
iframe.src = '';
});
});
// Also hide popup if mouse moves out of popup itself
popup.addEventListener('mouseleave', () => {
popup.style.display = 'none';
iframe.src = '';
});
});
</script>
{% endblock %}

32
templates/register.html Normal file
View File

@@ -0,0 +1,32 @@
{% extends 'base.html' %}
{% block title %}Register - Digitale Liturgie{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<div class="max-w-md mx-auto">
<h2 class="text-3xl font-semibold mb-6">Register</h2>
<form method="post" class="space-y-6">
<div>
<label for="username" class="block mb-3 font-semibold text-gray-700">Emailadres</label>
<input type="email" id="username" name="username" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="password" class="block mb-3 font-semibold text-gray-700">Wachtwoord</label>
<input type="password" id="password" name="password" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="church" class="block mb-3 font-semibold text-gray-700">Kerk</label>
<input type="text" id="church" name="church" placeholder="Kies of maak een kerk" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<button type="submit" class="bg-[#f7d91a] text-black w-full rounded-md py-4 font-semibold shadow hover:bg-yellow-300 transition">Register</button>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,282 @@
{% extends 'base.html' %}
{% block title %}{% if schedule %}Schema Bijwerken{% else %}Schema Toevoegen{% endif %}{% endblock %}
{% block content %}
<style>
.preview-container {
display: flex;
gap: 2rem;
align-items: flex-start;
flex-wrap: wrap;
max-width: 900px;
margin: 0 auto 2rem auto;
background: white;
padding: 1rem;
border-radius: 0.5rem;
}
.preview-wrapper {
width: 250px;
height: 444px;
background: #222;
border-radius: 0.5rem;
overflow: hidden;
position: relative;
background-size: cover;
background-position: center;
}
.liturgie-board {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 0.6vh;
padding-left: 5%;
position: relative;
z-index: 2;
}
.liturgie-line {
width: 90%;
font-size: 28px;
line-height: 1.2;
text-shadow: 2px 2px 8px #000, 0 0 4px #000;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0;
padding: 0;
color: #fff;
text-align: left;
outline: none;
cursor: text;
user-select: text;
}
/* Highlight for drag-over between lines in preview */
.liturgie-line.line-over {
background: rgba(255,255,255,0.22);
border: 2px dashed #fff;
box-sizing: border-box;
outline: none;
transition: background 0.12s, border 0.12s;
}
.liturgie-line.swap-animate {
animation: swap-pulse 0.35s;
}
@keyframes swap-pulse {
0% { background: #ffe066; }
60% { background: #fff3cd; }
100% { background: none; }
}
.input-section {
flex: 1 1 260px;
min-width: 260px;
display: flex;
flex-direction: column;
gap: 1rem;
}
.input-section label {
font-weight: 700;
color: #374151;
display: block;
margin-bottom: 0.25rem;
}
.input-section input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-size: 1rem;
outline: none;
box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.input-section input:focus {
border-color: #2563eb;
box-shadow: 0 0 5px #93c5fd;
}
.save-button {
padding: 0.75rem 1.25rem;
background-color: #16a34a;
color: white;
border-radius: 0.375rem;
font-weight: 700;
border: none;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
margin-top: auto;
}
.save-button:hover {
background-color: #15803d;
}
@media (max-width: 767px) {
.preview-container {
flex-direction: column;
max-width: 100%;
margin-bottom: 1rem;
}
.preview-wrapper {
width: 200px;
height: 355px;
margin: 0 auto 1rem auto;
}
.input-section {
min-width: unset;
}
}
</style>
<h1 class="text-3xl font-bold mb-6 text-center">{% if schedule %}Schema Bijwerken{% else %}Schema Toevoegen{% endif %}</h1>
<form method="post" id="scheduleForm">
<div class="preview-container">
<!-- Left: Live preview with editable lines -->
<div class="preview-wrapper" style="font-family: 'Lora', serif; position: relative; background-image: {% if schedule and schedule.boards_list|length > 0 %}
{% set board_bg = schedule.boards_list[0].background_image if schedule.boards_list[0].background_image else None %}
{% if board_bg %}
url('{% if board_bg.startswith('default_') %}{{ url_for('static', filename=board_bg) }}{% else %}{{ url_for('uploaded_file', filename=board_bg) }}{% endif %}')
{% else %}none
{% endif %}
{% else %}none
{% endif %}; background-color:#222; background-size: cover; background-position: center;">
<div class="liturgie-bg-overlay" style="
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 1;
pointer-events: none;
"></div>
<div class="liturgie-board" contenteditable="false" style="position: relative; z-index: 2; font-family: 'Lora', serif;">
{% set lines = schedule.content.split('\n') if schedule and schedule.content else [' ']*10 %}
{% for i in range(10) %}
<div class="liturgie-line" contenteditable="true" data-line="line{{ i+1 }}" spellcheck="false">{{ lines[i] if i < lines|length else ' ' }}</div>
{% endfor %}
</div>
</div>
<!-- Right: Input fields for schedule name, date, time, board assignment, and save button -->
<div class="input-section">
<label for="name">Naam:</label>
<input type="text" id="name" name="name" required value="{{ schedule.name if schedule else '' }}">
<label for="date">Datum:</label>
<input type="date" id="date" name="date" required value="{{ schedule.date.strftime('%Y-%m-%d') if schedule and schedule.date else '' }}">
<div style="display: flex; gap: 1rem; align-items: center;">
<div style="flex: 1;">
<label for="start_time">Starttijd:</label>
<input type="time" id="start_time" name="start_time" required value="{{ schedule.start_time.strftime('%H:%M') if schedule and schedule.start_time else '' }}">
</div>
<div style="flex: 1;">
<label for="end_time">Eindtijd:</label>
<input type="time" id="end_time" name="end_time" required value="{{ schedule.end_time.strftime('%H:%M') if schedule and schedule.end_time else '' }}">
</div>
</div>
<label>Toegewezen borden:</label>
<div class="overflow-hidden rounded-lg max-h-56 overflow-y-auto mb-6 border border-gray-200">
<table class="min-w-full divide-y divide-gray-200 border-collapse">
<thead class="bg-gray-50">
<tr class="divide-x divide-gray-200">
<th scope="col" class="py-3.5 pl-4 pr-4 text-left text-sm font-semibold text-gray-900 sm:pl-6">Naam</th>
<th scope="col" class="px-4 py-3.5 text-center text-sm font-semibold text-gray-900">Toewijzen</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{% for board in boards %}
<tr class="divide-x divide-gray-200 hover:bg-gray-50 cursor-pointer">
<td class="whitespace-nowrap py-4 pl-4 pr-4 text-sm font-semibold text-gray-900 sm:pl-6">{{ board.name }}</td>
<td class="whitespace-nowrap px-4 py-4 text-center text-sm font-semibold text-gray-900 flex justify-center items-center">
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" value="{{ board.id }}" name="boards" class="sr-only peer" {% if schedule and board in schedule.boards_list %}checked{% endif %}>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:bg-blue-600 transition-all duration-300"></div>
<div class="absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform duration-300 peer-checked:translate-x-5"></div>
</label>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<button type="submit" class="bg-[#f7d91a] text-black px-8 py-3 rounded font-semibold hover:bg-yellow-300 transition">Opslaan</button>
</div>
</div>
<!-- Hidden inputs to store lines for form submission -->
{% for i in range(1, 11) %}
<input type="hidden" name="line{{ i }}" id="hidden-line{{ i }}" value="">
{% endfor %}
</form>
<script>
// --- Line drag-and-swap logic ---
document.addEventListener('DOMContentLoaded', function () {
let dragSrc = null;
const lines = document.querySelectorAll('.liturgie-line[data-line]');
lines.forEach(line => {
line.addEventListener('dragstart', function (e) {
dragSrc = this;
this.classList.add('dragging');
});
line.addEventListener('dragend', function (e) {
this.classList.remove('dragging');
dragSrc = null;
lines.forEach(l => l.classList.remove('line-over'));
});
line.addEventListener('dragover', function (e) {
e.preventDefault();
if (this !== dragSrc) this.classList.add('line-over');
});
line.addEventListener('dragleave', function (e) {
this.classList.remove('line-over');
});
line.addEventListener('drop', function (e) {
e.preventDefault();
this.classList.remove('line-over');
if (dragSrc && dragSrc !== this) {
// Only swap textContent, not the whole element
const tmp = this.textContent;
this.textContent = dragSrc.textContent;
dragSrc.textContent = tmp;
// animation for both lines
this.classList.add('swap-animate');
dragSrc.classList.add('swap-animate');
setTimeout(() => {
this.classList.remove('swap-animate');
dragSrc.classList.remove('swap-animate');
}, 400);
}
});
// Make lines draggable
line.setAttribute('draggable', 'true');
});
});
// --- End drag-and-swap logic ---
// block enter key
document.getElementById('scheduleForm').addEventListener('keydown', function (event) {
if (event.key === 'Enter') {
event.preventDefault();
}
});
// end of block enter key
document.getElementById('scheduleForm').addEventListener('submit', function(e) {
// Copy content from editable divs to hidden inputs
for (let i = 1; i <= 10; i++) {
let div = document.querySelector('.liturgie-line[data-line="line' + i + '"]');
let hiddenInput = document.getElementById('hidden-line' + i);
if (div && hiddenInput) {
hiddenInput.value = div.textContent.trim() || ' ';
}
}
});
// Optional: keyboard navigation and basic shortcuts could be added here
</script>
{% endblock %}

View File

@@ -0,0 +1,44 @@
{% extends 'base.html' %}
{% block title %}Achtergrondafbeelding wijzigen - Digitale Liturgie{% endblock %}
{% block content %}
<div class="max-w-lg mx-auto">
<h2 class="text-3xl font-semibold mb-6">Achtergrondafbeelding wijzigen voor: {{ board.name }}</h2>
<div class="flex space-x-8">
<form method="post" enctype="multipart/form-data" class="space-y-6 flex-1">
<div>
<label for="background" class="block mb-2 font-semibold text-gray-700">Kies een afbeelding</label>
<input type="file" id="background" name="background" accept="image/*" required class="w-full rounded-md border border-gray-300 p-3 text-base placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<button type="submit" class="bg-[#f7d91a] text-black w-full rounded-md py-3 font-semibold shadow hover:bg-yellow-300 transition">Uploaden</button>
<a href="{{ url_for('edit_board', board_id=board.id) }}" class="block text-center bg-white text-black border border-black rounded-md py-3 mt-2 font-semibold shadow hover:bg-gray-100 transition w-full">Annuleren</a>
</form>
{% if board.background_image %}
<div class="flex-1">
<p class="mb-2">Huidige achtergrond:</p>
<img src="{{ url_for('uploaded_file', filename=board.background_image) }}" alt="Achtergrond" class="rounded-lg max-w-full max-h-72 object-contain">
</div>
{% endif %}
</div>
<div class="mt-10">
<h3 class="text-xl font-semibold mb-4">Kies een standaard achtergrond:</h3>
<div class="grid grid-cols-4 gap-4">
<form method="post" class="inline-block">
<input type="hidden" name="default_background" value="default_wall_1.jpg">
<img src="/static/default_wall_1.jpg" alt="Standaard achtergrond 1" class="cursor-pointer rounded-lg border border-gray-300 hover:border-blue-600 transition" onclick="this.closest('form').submit()">
</form>
<form method="post" class="inline-block">
<input type="hidden" name="default_background" value="default_wall_2.jpg">
<img src="/static/default_wall_2.jpg" alt="Standaard achtergrond 2" class="cursor-pointer rounded-lg border border-gray-300 hover:border-blue-600 transition" onclick="this.closest('form').submit()">
</form>
<form method="post" class="inline-block">
<input type="hidden" name="default_background" value="default_wall_3.jpg">
<img src="/static/default_wall_3.jpg" alt="Standaard achtergrond 3" class="cursor-pointer rounded-lg border border-gray-300 hover:border-blue-600 transition" onclick="this.closest('form').submit()">
</form>
<form method="post" class="inline-block">
<input type="hidden" name="default_background" value="default_wall_4.jpg">
<img src="/static/default_wall_4.jpg" alt="Standaard achtergrond 4" class="cursor-pointer rounded-lg border border-gray-300 hover:border-blue-600 transition" onclick="this.closest('form').submit()">
</form>
</div>
</div>
</div>
{% endblock %}

4
wsgi.py Normal file
View File

@@ -0,0 +1,4 @@
from app import app
if __name__ == '__main__':
app.run()