first commit
12
Dockerfile
Normal 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
@@ -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
@@ -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
@@ -0,0 +1,9 @@
|
||||
Flask
|
||||
Flask_SQLAlchemy
|
||||
Flask_Login
|
||||
flask_migrate
|
||||
Werkzeug
|
||||
SQLAlchemy
|
||||
itsdangerous
|
||||
python-dotenv
|
||||
gunicorn
|
||||
BIN
static/Hero.jpg
Normal file
|
After Width: | Height: | Size: 482 KiB |
BIN
static/default_logo.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
static/default_wall_1.jpg
Normal file
|
After Width: | Height: | Size: 851 KiB |
BIN
static/default_wall_2.jpg
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
static/default_wall_3.jpg
Normal file
|
After Width: | Height: | Size: 341 KiB |
BIN
static/default_wall_4.jpg
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
static/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
static/favicon.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
static/icons/board.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
static/icons/church.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
static/icons/cog.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
static/icons/group.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
static/icons/task.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
27
templates/add_board.html
Normal 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 %}
|
||||
107
templates/admin_dashboard.html
Normal 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 %}
|
||||
18
templates/admin_login.html
Normal 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 %}
|
||||
50
templates/admin_users.html
Normal 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
@@ -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">
|
||||
© {{ current_year }} PsalmbordOnline | Contact: <a href="mailto:info@psalmbord.online" class="text-blue-600 hover:underline">info@psalmbord.online</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
48
templates/board_details.html
Normal 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 %}
|
||||
45
templates/board_schedules.html
Normal 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 %}
|
||||
36
templates/change_password.html
Normal 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 %}
|
||||
104
templates/church_settings.html
Normal 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 %}
|
||||
228
templates/display_board.html
Normal 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
@@ -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 %}
|
||||
43
templates/global_schedules.html
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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 %}
|
||||
282
templates/schedule_form.html
Normal 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 %}
|
||||
44
templates/upload_background.html
Normal 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 %}
|
||||